yogo-db 0.2.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.
- data/.document +5 -0
- data/.gitignore +24 -0
- data/.rspec +1 -0
- data/Gemfile +36 -0
- data/Gemfile.lock +213 -0
- data/LICENSE +20 -0
- data/README.rdoc +17 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/config.ru +27 -0
- data/config/database.yml +19 -0
- data/config/datamapper.rb +9 -0
- data/features/step_definitions/yogo-db_steps.rb +0 -0
- data/features/support/env.rb +2 -0
- data/features/yogo-db.feature +9 -0
- data/lib/yogo-db.rb +4 -0
- data/lib/yogo/data/default_methods.rb +89 -0
- data/lib/yogo/data_app.rb +50 -0
- data/lib/yogo/model/schema.rb +138 -0
- data/lib/yogo/operations.rb +2 -0
- data/lib/yogo/operations/basic.rb +31 -0
- data/lib/yogo/operations/file.rb +44 -0
- data/lib/yogo/rack/model_lookup.rb +37 -0
- data/lib/yogo/schema_app.rb +43 -0
- data/spec/factories/schema.rb +5 -0
- data/spec/helpers/request_helper.rb +30 -0
- data/spec/model/schema_spec.rb +136 -0
- data/spec/spec_helper.rb +37 -0
- data/spec/yogo/data_app_spec.rb +32 -0
- data/spec/yogo/model_lookup_spec.rb +78 -0
- data/spec/yogo/schema_app_spec.rb +105 -0
- data/yogo-db.gemspec +115 -0
- metadata +259 -0
File without changes
|
data/lib/yogo-db.rb
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
module Yogo
|
2
|
+
module Data
|
3
|
+
module DefaultMethods
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:extend, ClassMethods)
|
7
|
+
base.send(:include, InstanceMethods)
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
def schema
|
12
|
+
class_variable_get(:@@schema)
|
13
|
+
end
|
14
|
+
|
15
|
+
def parse_json(body)
|
16
|
+
json = JSON.parse(body)
|
17
|
+
props = labeled_properties
|
18
|
+
ret = {}
|
19
|
+
# I don't think we care about other items passed back.
|
20
|
+
json['data'].each_pair do |key,value|
|
21
|
+
property = props.select{|p| p.options['label'] == key || p.name.to_s == key}
|
22
|
+
return nil unless property.length == 1
|
23
|
+
# raise Exception, "There shouldn't be more the one property with the same label"
|
24
|
+
ret[property.first.name] = value
|
25
|
+
end
|
26
|
+
return ret
|
27
|
+
end
|
28
|
+
|
29
|
+
def labeled_properties
|
30
|
+
properties.select{|p| !p.options['label'].nil? }
|
31
|
+
end
|
32
|
+
|
33
|
+
def unlabeled_properties
|
34
|
+
properties.select{|p| p.options['label'].nil? }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
module InstanceMethods
|
39
|
+
def to_url
|
40
|
+
raise Exception, "Need to save before item has a url" if new?
|
41
|
+
"/data/" << self.model.name << "/" << yogo_id.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def attributes_by_label
|
45
|
+
attributes = {}
|
46
|
+
properties.each do |property|
|
47
|
+
label = property.options['label']
|
48
|
+
next if label.nil?
|
49
|
+
name = property.name
|
50
|
+
next unless model.public_method_defined?(name)
|
51
|
+
attributes[label] = __send__(name)
|
52
|
+
end
|
53
|
+
return attributes
|
54
|
+
end
|
55
|
+
|
56
|
+
def labeled_properties
|
57
|
+
self.class.labeled_properties
|
58
|
+
end
|
59
|
+
|
60
|
+
def unlabeled_properties
|
61
|
+
self.class.unlabeled_properties
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_json(*args)
|
65
|
+
options = args.first || {}
|
66
|
+
options = options.to_h if options.respond_to?(:to_h)
|
67
|
+
|
68
|
+
result = as_json(*args)
|
69
|
+
|
70
|
+
if options.fetch(:to_json, true)
|
71
|
+
result.to_json
|
72
|
+
else
|
73
|
+
result
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def as_json(*a)
|
78
|
+
data = labeled_properties.reduce({}){ |result, property| result.merge(property.name => __send__(property.name)) }
|
79
|
+
default_data = unlabeled_properties.reduce({}){ |result, property| result.merge(property.name => __send__(property.name)) }
|
80
|
+
default_data.merge({
|
81
|
+
:url => self.to_url,
|
82
|
+
:data => data
|
83
|
+
})
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
module Yogo
|
4
|
+
class DataApp < ::Sinatra::Base
|
5
|
+
before do
|
6
|
+
content_type :json
|
7
|
+
end
|
8
|
+
|
9
|
+
get '/data/:model_id/?' do
|
10
|
+
{ :content => env['yogo.resource'].all.to_json(:to_json => false) }.to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
get '/data/:model_id/:id' do
|
14
|
+
resource = env['yogo.resource']
|
15
|
+
item = resource.get(params[:id])
|
16
|
+
|
17
|
+
{ :content => item }.to_json
|
18
|
+
end
|
19
|
+
|
20
|
+
post '/data/:model_id' do
|
21
|
+
resource = env['yogo.resource']
|
22
|
+
opts = resource.parse_json(request.body.read) rescue nil
|
23
|
+
|
24
|
+
halt(401, 'Invalid Format') if opts.nil?
|
25
|
+
|
26
|
+
item = resource.new(opts)
|
27
|
+
|
28
|
+
halt(500, 'Could not save item') unless item.save
|
29
|
+
|
30
|
+
response['Location'] = item.to_url
|
31
|
+
response.status = 201
|
32
|
+
end
|
33
|
+
|
34
|
+
put '/schema/:model_id/:id' do
|
35
|
+
resource = env['yogo.resource']
|
36
|
+
item = resource.get(params[:id])
|
37
|
+
opts = resource.parse_json(request.body.read) rescue nil
|
38
|
+
halt(401, 'Invalid Format') if opts.nil?
|
39
|
+
|
40
|
+
halt(500, 'Could not update schema') unless item.update(opts)
|
41
|
+
|
42
|
+
{ :content => item }.to_json
|
43
|
+
end
|
44
|
+
|
45
|
+
delete '/schema/:model_id/:id' do
|
46
|
+
env['yogo.resource'].get(params[:id]).destroy
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require 'dm-types'
|
2
|
+
require 'yogo/datamapper/model/configuration'
|
3
|
+
require 'yogo/operations'
|
4
|
+
|
5
|
+
class Schema
|
6
|
+
include ::DataMapper::Resource
|
7
|
+
# include Yogo::DataMapper::Model::Configuration
|
8
|
+
include Dataflow
|
9
|
+
|
10
|
+
# Common::Properties::UUIDKey[self]
|
11
|
+
property :id, ::DataMapper::Property::UUID, {:key => true, :default => lambda{|*args| UUIDTools::UUID.timestamp_create}}
|
12
|
+
|
13
|
+
property :name, String, :required => true, :key => true, :unique => true
|
14
|
+
|
15
|
+
property :operation_definitions, Yaml, :lazy => false, :default => []
|
16
|
+
|
17
|
+
before :save, :reset_data_model
|
18
|
+
before :destroy, :destroy_data_model_bucket
|
19
|
+
|
20
|
+
# How do we know what variables need to be constantized.
|
21
|
+
# Also, we might have more or fewer then 3 parameters in the operation
|
22
|
+
# def operation_definitions
|
23
|
+
# @__operation_definitions ||= attribute_get(:op_defs)
|
24
|
+
# end
|
25
|
+
|
26
|
+
# Set only the operations we want, don't add to them
|
27
|
+
def operations=(ops)
|
28
|
+
# Clear the current operations
|
29
|
+
self.operation_definitions = []
|
30
|
+
# Load operations with the set given
|
31
|
+
ops.each{|op| operation(*op) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def operation(op_name, *args)
|
35
|
+
op_def = replace_nil_items([op_name.to_s, args].flatten)
|
36
|
+
|
37
|
+
unless self.operation_definitions.include?(op_def)
|
38
|
+
# We need to dup this else the model doesn't get marked as dirty and won't save.
|
39
|
+
self.operation_definitions = self.operation_definitions.dup << op_def
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def data_model
|
44
|
+
@data_model ||= by_need { gen_model }
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_url
|
48
|
+
"/schema/#{self.name}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_json(*args)
|
52
|
+
options = args.first || {}
|
53
|
+
options = options.to_h if options.respond_to?(:to_h)
|
54
|
+
|
55
|
+
result = as_json(*args)
|
56
|
+
|
57
|
+
if options.fetch(:to_json, true)
|
58
|
+
result.to_json
|
59
|
+
else
|
60
|
+
result
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def as_json(*a)
|
65
|
+
{
|
66
|
+
:guid => self.to_url,
|
67
|
+
:name => self.name,
|
68
|
+
:operations => self.operation_definitions,
|
69
|
+
}
|
70
|
+
end
|
71
|
+
|
72
|
+
REQUIRED_JSON_KEYS=[:name, :operations]
|
73
|
+
|
74
|
+
def self.parse_json(body)
|
75
|
+
json = JSON.parse(body)
|
76
|
+
|
77
|
+
ret = { :name => json['name'], :operations => json['operations'] }
|
78
|
+
|
79
|
+
return nil if REQUIRED_JSON_KEYS.any? { |r| ret[r].nil? }
|
80
|
+
|
81
|
+
ret
|
82
|
+
end
|
83
|
+
|
84
|
+
def to_proc
|
85
|
+
base_op = Yogo::DataMapper::Model::Operations['add/default_properties']
|
86
|
+
# base_op = Yogo::DataMapper::Model::Operations['add/yogo_methods']
|
87
|
+
ops = operation_definitions.map{|op_def| Yogo::DataMapper::Model::Operations[op_def.first] }
|
88
|
+
partial_ops = []
|
89
|
+
ops.each_with_index do |op, i|
|
90
|
+
next unless op
|
91
|
+
partial_ops[i] = op.partial(X, *operation_definitions[i][1..-1])
|
92
|
+
end
|
93
|
+
partial_ops << Yogo::DataMapper::Model::Operations['add/yogo_methods']
|
94
|
+
partial_ops.compact!
|
95
|
+
partial_ops.reduce(base_op){|composed, partial_op| composed * partial_op}
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def gen_model
|
102
|
+
id = attribute_get(:id)
|
103
|
+
model_name = attribute_get(:name)
|
104
|
+
base_model = ::DataMapper::Model.new do
|
105
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
106
|
+
|
107
|
+
def self.default_storage_name
|
108
|
+
"#{id}"
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.name
|
112
|
+
"#{model_name}"
|
113
|
+
end
|
114
|
+
RUBY
|
115
|
+
end
|
116
|
+
|
117
|
+
base_model.class_variable_set(:@@schema, self)
|
118
|
+
self.to_proc[base_model]
|
119
|
+
base_model.auto_upgrade!
|
120
|
+
return base_model
|
121
|
+
end
|
122
|
+
|
123
|
+
def reset_data_model
|
124
|
+
@data_model = nil
|
125
|
+
end
|
126
|
+
|
127
|
+
def destroy_data_model_bucket
|
128
|
+
data_model.auto_migrate_down!
|
129
|
+
end
|
130
|
+
|
131
|
+
def generate_column_uuid
|
132
|
+
"col_#{UUIDTools::UUID.timestamp_create.to_s.gsub('-', '_')}"
|
133
|
+
end
|
134
|
+
|
135
|
+
def replace_nil_items(array)
|
136
|
+
array.map{|i| i.nil? ? generate_column_uuid : i }
|
137
|
+
end
|
138
|
+
end # Configuration
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'yogo/operation'
|
2
|
+
require 'yogo/data/default_methods'
|
3
|
+
|
4
|
+
# Move into DataMapper::Property extension file
|
5
|
+
unless ::DataMapper::Property.accepted_options.include?('label')
|
6
|
+
::DataMapper::Property.accept_options('label')
|
7
|
+
end
|
8
|
+
|
9
|
+
module Operations
|
10
|
+
Yogo::DataMapper::Model::Operations['add/yogo_methods'] = Yogo::Op.on(::DataMapper::Model) do |model|
|
11
|
+
|
12
|
+
# model.define some properties
|
13
|
+
|
14
|
+
model.send(:include, Yogo::Data::DefaultMethods)
|
15
|
+
|
16
|
+
model
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
Yogo::DataMapper::Model::Operations['add/yogo_id_property'] =
|
21
|
+
Yogo::DataMapper::Model::Operations['add/property'].partial(X, :yogo_id, 'Serial', {})
|
22
|
+
Yogo::DataMapper::Model::Operations['add/created_at_property'] =
|
23
|
+
Yogo::DataMapper::Model::Operations['add/property'].partial(X, :created_at, 'DateTime', {})
|
24
|
+
Yogo::DataMapper::Model::Operations['add/updated_at_property'] =
|
25
|
+
Yogo::DataMapper::Model::Operations['add/property'].partial(X, :updated_at, 'DateTime', {})
|
26
|
+
|
27
|
+
Yogo::DataMapper::Model::Operations['add/default_properties'] =
|
28
|
+
Yogo::DataMapper::Model::Operations['add/yogo_id_property'] *
|
29
|
+
Yogo::DataMapper::Model::Operations['add/created_at_property'] *
|
30
|
+
Yogo::DataMapper::Model::Operations['add/updated_at_property']
|
31
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Operation to add a file type here
|
2
|
+
|
3
|
+
Yogo::DataMapper::Model::Operations['add/file'] = Yogo::Op.on(::DataMapper::Model) do |model, name, options|
|
4
|
+
|
5
|
+
uploader = Class.new(CarrierWave::Uploader::Base)
|
6
|
+
uploader.storage(:file)
|
7
|
+
uploader.class_eval %{
|
8
|
+
def store_dir
|
9
|
+
File.join('assets', '#{model.schema.id}', '#{name}')
|
10
|
+
end
|
11
|
+
|
12
|
+
def filename
|
13
|
+
# Digest::MD5.hexdigest(self.read)
|
14
|
+
UUIDTools::UUID.timestamp_create
|
15
|
+
end
|
16
|
+
}, __FILE__, __LINE__+1
|
17
|
+
|
18
|
+
# model.define some properties
|
19
|
+
model.class_eval do
|
20
|
+
without_auto_validations do
|
21
|
+
# property :content_type, String
|
22
|
+
# property :description, String
|
23
|
+
property "#{name}_asset_file".to_sym, String, options
|
24
|
+
property "#{name}_original_filename".to_sym, String
|
25
|
+
end
|
26
|
+
|
27
|
+
mount_uploader name.to_sym, uploader, :mount_on => "#{name}_asset_file".to_sym
|
28
|
+
after "#{name}=".to_sym, "write_#{name}_identifier".to_sym
|
29
|
+
after "#{name}=".to_sym, "set_#{name}_original_filename".to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
model.class_eval %{
|
33
|
+
private
|
34
|
+
|
35
|
+
def set_#{name}_original_filename
|
36
|
+
original_filename = #{name}.send(:original_filename)
|
37
|
+
attribute_set(:#{name}_original_filename, original_filename)
|
38
|
+
end
|
39
|
+
}, __FILE__, __LINE__+1
|
40
|
+
|
41
|
+
|
42
|
+
model
|
43
|
+
|
44
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require 'dm-core'
|
3
|
+
require 'yogo/model/schema'
|
4
|
+
|
5
|
+
module Yogo
|
6
|
+
module Rack
|
7
|
+
class ModelLookup
|
8
|
+
def initialize(app, options = {})
|
9
|
+
paths = options[:paths] || ['data']
|
10
|
+
@app = app
|
11
|
+
@scope = options[:scope] || lambda { |*args| return Schema }
|
12
|
+
@base_regexp = /^\/(#{paths.join('|')})\/((\w|-|\s)+)/
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
if env['PATH_INFO'] =~ @base_regexp
|
17
|
+
model_id = $2
|
18
|
+
env['yogo.schema'], env['yogo.resource'] = get_model(model_id, env)
|
19
|
+
return [ 404, {'Content-Type' => 'text/plain'}, ["#{model_id} not found"] ] if env['yogo.schema'].nil?
|
20
|
+
end
|
21
|
+
|
22
|
+
@app.call(env)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def get_model(model_id, env)
|
28
|
+
config = @scope.call(env).first(:name => model_id)
|
29
|
+
|
30
|
+
unless config.nil?
|
31
|
+
return config, config.data_model
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
end # ModelLookup
|
36
|
+
end # Rack
|
37
|
+
end # Yogo
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'sinatra'
|
2
|
+
|
3
|
+
module Yogo
|
4
|
+
class SchemaApp < ::Sinatra::Base
|
5
|
+
before do
|
6
|
+
content_type :json
|
7
|
+
end
|
8
|
+
|
9
|
+
get '/schema/?' do
|
10
|
+
{ :content => Schema.all.to_json(:to_json => false) }.to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
get '/schema/:model_id/?' do
|
14
|
+
{ :content => env['yogo.schema'] }.to_json
|
15
|
+
end
|
16
|
+
|
17
|
+
post '/schema/?' do
|
18
|
+
opts = Schema.parse_json(request.body.read) rescue nil
|
19
|
+
halt(401, 'Invalid Format') if opts.nil?
|
20
|
+
schema = Schema.new(opts)
|
21
|
+
|
22
|
+
halt(500, 'Could not save schema') unless schema.save
|
23
|
+
|
24
|
+
response['Location'] = schema.to_url
|
25
|
+
response.status = 201
|
26
|
+
end
|
27
|
+
|
28
|
+
put '/schema/:model_id' do
|
29
|
+
schema = env['yogo.schema']
|
30
|
+
opts = Schema.parse_json(request.body.read) rescue nil
|
31
|
+
halt(401, 'Invalid Format') if opts.nil?
|
32
|
+
|
33
|
+
halt(500, 'Could not update schema') unless schema.update(opts)
|
34
|
+
|
35
|
+
{ :content => schema }.to_json
|
36
|
+
end
|
37
|
+
|
38
|
+
delete '/schema/:model_id' do
|
39
|
+
env['yogo.schema'].destroy
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|