instedd-pigeon 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +25 -0
- data/.rvmrc +1 -0
- data/Gemfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +140 -0
- data/Rakefile +1 -0
- data/app/assets/images/pigeon/android_local_gateway_preview.png +0 -0
- data/app/assets/images/pigeon/android_local_gateway_qr.png +0 -0
- data/app/assets/images/pigeon/other_local_gateway_preview.png +0 -0
- data/app/assets/javascripts/pigeon/qst-server-wizard.js.coffee +58 -0
- data/app/assets/javascripts/pigeon/template.js.coffee +43 -0
- data/app/assets/javascripts/pigeon/twitter-template.js.coffee +38 -0
- data/app/assets/javascripts/pigeon/wizard.js.coffee +95 -0
- data/app/assets/javascripts/pigeon.js +2 -0
- data/app/assets/stylesheets/pigeon.css.sass +13 -0
- data/app/controllers/pigeon/twitter_controller.rb +44 -0
- data/app/helpers/.gitkeep +0 -0
- data/app/helpers/pigeon/channel_helper.rb +93 -0
- data/app/helpers/pigeon/renderer/base.rb +172 -0
- data/app/helpers/pigeon/renderer/channel_renderer.rb +58 -0
- data/app/helpers/pigeon/renderer.rb +8 -0
- data/app/helpers/pigeon/tag_helper.rb +26 -0
- data/app/helpers/pigeon/template_helper.rb +20 -0
- data/app/helpers/pigeon/text_helper.rb +16 -0
- data/app/models/.gitkeep +0 -0
- data/app/models/pigeon/channel.rb +220 -0
- data/app/models/pigeon/channel_attribute.rb +64 -0
- data/app/models/pigeon/channel_schema.rb +113 -0
- data/app/models/pigeon/nested_attribute.rb +23 -0
- data/app/models/pigeon/nested_scopes.rb +34 -0
- data/app/models/pigeon/nuntium_channel.rb +92 -0
- data/app/models/pigeon/verboice_channel.rb +82 -0
- data/app/views/pigeon/twitter/callback.html.erb +6 -0
- data/config/routes.rb +4 -0
- data/config/schemas/nuntium/nuntium.yml +210 -0
- data/config/schemas/nuntium/qst-server.yml +180 -0
- data/config/schemas/nuntium/twitter.yml +56 -0
- data/config/schemas/verboice/verboice.yml +76 -0
- data/lib/pigeon/engine.rb +30 -0
- data/lib/pigeon/errors.rb +28 -0
- data/lib/pigeon/initializer.rb +13 -0
- data/lib/pigeon/nuntium.rb +10 -0
- data/lib/pigeon/verboice.rb +10 -0
- data/lib/pigeon/version.rb +3 -0
- data/lib/pigeon.rb +25 -0
- data/pigeon.gemspec +24 -0
- data/spec/data/test_schemas.yml +21 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +59 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +3 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/helpers/pigeon/channel_helper_spec.rb +173 -0
- data/spec/helpers/pigeon/renderer/base_spec.rb +109 -0
- data/spec/helpers/pigeon/renderer/channel_renderer_spec.rb +61 -0
- data/spec/helpers/pigeon/tag_helper_spec.rb +36 -0
- data/spec/helpers/pigeon/template_helper_spec.rb +57 -0
- data/spec/models/pigeon/channel_attribute_spec.rb +98 -0
- data/spec/models/pigeon/channel_schema_spec.rb +63 -0
- data/spec/models/pigeon/channel_spec.rb +205 -0
- data/spec/models/pigeon/nuntium_channel_spec.rb +53 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/active_model_lint.rb +15 -0
- data/spec/support/test_schemas.rb +12 -0
- metadata +254 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
module Pigeon
|
2
|
+
module Renderer
|
3
|
+
class Base
|
4
|
+
include Sprockets::Helpers::RailsHelper
|
5
|
+
include Sprockets::Helpers::IsolatedHelper
|
6
|
+
include ActionView::Helpers::OutputSafetyHelper
|
7
|
+
include ActionView::Helpers::TextHelper
|
8
|
+
include ActionView::Helpers::TagHelper
|
9
|
+
include ActionView::Helpers::AssetTagHelper
|
10
|
+
|
11
|
+
class << self
|
12
|
+
def handle(command, handler)
|
13
|
+
@handlers ||= {}
|
14
|
+
@handlers[command.to_s] = handler
|
15
|
+
end
|
16
|
+
|
17
|
+
def find_handler(command)
|
18
|
+
@handlers ||= {}
|
19
|
+
@handlers.fetch(command.to_s) do |command|
|
20
|
+
if superclass.respond_to?(:find_handler)
|
21
|
+
self.superclass.find_handler(command)
|
22
|
+
else
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :delegate
|
30
|
+
|
31
|
+
handle :raw, :render_raw
|
32
|
+
handle :template, :render_template_command
|
33
|
+
handle :wizard, :render_template_command
|
34
|
+
handle :page, :render_template_command
|
35
|
+
|
36
|
+
def render(v)
|
37
|
+
return '' if v.empty?
|
38
|
+
|
39
|
+
if v.first.starts_with? '@'
|
40
|
+
render_at_command(v)
|
41
|
+
else
|
42
|
+
render_vector(v)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def render_at_command(v)
|
47
|
+
command = v.first[1..-1]
|
48
|
+
handler = find_handler(command)
|
49
|
+
if !handler.nil?
|
50
|
+
dispatch_command_handler(handler, v)
|
51
|
+
elsif delegate.present? && delegate.respond_to?(:call)
|
52
|
+
delegate.call(v)
|
53
|
+
else
|
54
|
+
''
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_options(data)
|
59
|
+
if data[1].is_a?(Hash)
|
60
|
+
[data[0], data[1], data.drop(2)]
|
61
|
+
else
|
62
|
+
[data[0], {}, data.drop(1)]
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def render_vector(v)
|
67
|
+
selector, user_options, content = extract_options(v)
|
68
|
+
tag_name, options = parse_tag_name(selector)
|
69
|
+
|
70
|
+
options.update(user_options.with_indifferent_access) do |key, oldval, newval|
|
71
|
+
case key.to_s
|
72
|
+
when "id"
|
73
|
+
oldval
|
74
|
+
when "class"
|
75
|
+
oldval + " " + newval
|
76
|
+
else
|
77
|
+
newval
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if tag_name.downcase == 'img'
|
82
|
+
if URI(path = options[:src]).relative?
|
83
|
+
options[:src] = image_path(path)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
if content.empty?
|
88
|
+
if %w(br meta img link script hr).include? tag_name.downcase
|
89
|
+
tag tag_name, options
|
90
|
+
else
|
91
|
+
content_tag tag_name, '', options
|
92
|
+
end
|
93
|
+
else
|
94
|
+
content_html = safe_join(content.map do |elt|
|
95
|
+
case elt
|
96
|
+
when Array
|
97
|
+
render(elt)
|
98
|
+
else
|
99
|
+
elt
|
100
|
+
end
|
101
|
+
end)
|
102
|
+
content_tag tag_name, content_html, options
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def render_raw(data)
|
107
|
+
data.drop(1).join.html_safe
|
108
|
+
end
|
109
|
+
|
110
|
+
def render_template_command(v)
|
111
|
+
command, options, content = extract_options(v)
|
112
|
+
|
113
|
+
options = options.inject({}) do |options, (key, value)|
|
114
|
+
if %w(class style).include?(key.to_s) || key.to_s.starts_with?('data-')
|
115
|
+
options[key] = value
|
116
|
+
else
|
117
|
+
options["data-#{key.to_s}"] = value
|
118
|
+
end
|
119
|
+
options
|
120
|
+
end
|
121
|
+
if %w(@template @wizard).include?(command)
|
122
|
+
options["data-application-name"] ||= Pigeon.config.application_name
|
123
|
+
options["data-twitter-path"] = Pigeon::Engine.routes.url_helpers.twitter_path
|
124
|
+
end
|
125
|
+
|
126
|
+
case command
|
127
|
+
when '@wizard'
|
128
|
+
render ["div.pigeon.pigeon_wizard", options] + content
|
129
|
+
when '@page'
|
130
|
+
render ["div.pigeon_wizard_page", options] + content
|
131
|
+
when '@template'
|
132
|
+
render ["div.pigeon.pigeon_template", options] + content
|
133
|
+
else
|
134
|
+
''
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
def parse_tag_name(input)
|
141
|
+
tag_name = input.scan(/\A[^#.]*/).first
|
142
|
+
tag_name = 'div' if tag_name.blank?
|
143
|
+
options = {}.with_indifferent_access
|
144
|
+
classes = []
|
145
|
+
input.scan(/[#.][^#.]+/) do |tok|
|
146
|
+
case tok.first
|
147
|
+
when '#'
|
148
|
+
options[:id] = tok[1..-1]
|
149
|
+
when '.'
|
150
|
+
classes << tok[1..-1]
|
151
|
+
end
|
152
|
+
end
|
153
|
+
options[:class] = classes.join(' ') unless classes.empty?
|
154
|
+
|
155
|
+
[tag_name, options]
|
156
|
+
end
|
157
|
+
|
158
|
+
def find_handler(command)
|
159
|
+
self.class.find_handler(command)
|
160
|
+
end
|
161
|
+
|
162
|
+
def dispatch_command_handler(handler, data)
|
163
|
+
if handler.is_a?(Symbol)
|
164
|
+
self.send(handler, data)
|
165
|
+
elsif handler.is_a?(Proc)
|
166
|
+
handler.class(data)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module Pigeon
|
2
|
+
module Renderer
|
3
|
+
class ChannelRenderer < Base
|
4
|
+
include Pigeon::ChannelHelper
|
5
|
+
|
6
|
+
attr_accessor :output_buffer
|
7
|
+
attr_accessor :channel, :scope
|
8
|
+
|
9
|
+
handle :field, :render_attribute_command
|
10
|
+
handle :label, :render_attribute_command
|
11
|
+
handle :attr, :render_attribute_command
|
12
|
+
handle :hidden, :render_attribute_command
|
13
|
+
|
14
|
+
def initialize(channel = nil, scope = nil)
|
15
|
+
self.channel = channel
|
16
|
+
self.scope = scope
|
17
|
+
end
|
18
|
+
|
19
|
+
def render_attribute_command(v)
|
20
|
+
command, options, content = extract_options(v)
|
21
|
+
name = content.first
|
22
|
+
options[:scope] = scope if scope.present?
|
23
|
+
|
24
|
+
case command
|
25
|
+
when '@field'
|
26
|
+
pigeon_channel_attribute_field(channel, name, options)
|
27
|
+
when '@label'
|
28
|
+
pigeon_channel_attribute_label(channel, name, options)
|
29
|
+
when '@attr'
|
30
|
+
if options.include?(:class)
|
31
|
+
options[:class] << ' pigeon_attribute'
|
32
|
+
else
|
33
|
+
options[:class] = 'pigeon_attribute'
|
34
|
+
end
|
35
|
+
pigeon_channel_attribute(channel, name, options)
|
36
|
+
when '@hidden'
|
37
|
+
options = options.update({ :type => :hidden })
|
38
|
+
pigeon_channel_attribute_field(channel, name, options)
|
39
|
+
else
|
40
|
+
''
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def render_template_command(v)
|
45
|
+
command, options, content = extract_options(v)
|
46
|
+
if %w(@wizard @template).include?(command)
|
47
|
+
options["data-scope"] ||= scope if scope.present?
|
48
|
+
options["data-persisted"] = channel.persisted?
|
49
|
+
options["data-kind"] = channel.kind
|
50
|
+
options["data-name"] = channel.name
|
51
|
+
end
|
52
|
+
super([command, options] + content)
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Pigeon
|
2
|
+
module TagHelper
|
3
|
+
def pigeon_verboice_channel_kinds_for_select
|
4
|
+
options_for_select(pigeon_schema_options(VerboiceChannel.schemas))
|
5
|
+
end
|
6
|
+
|
7
|
+
def pigeon_nuntium_channel_kinds_for_select
|
8
|
+
options_for_select(pigeon_schema_options(NuntiumChannel.schemas))
|
9
|
+
end
|
10
|
+
|
11
|
+
def pigeon_channel_kinds_for_select
|
12
|
+
grouped_options_for_select({
|
13
|
+
"Voice Channel" =>
|
14
|
+
pigeon_schema_options(VerboiceChannel.schemas, 'verboice/'),
|
15
|
+
"Message Channel" =>
|
16
|
+
pigeon_schema_options(NuntiumChannel.schemas, 'nuntium/')
|
17
|
+
})
|
18
|
+
end
|
19
|
+
|
20
|
+
def pigeon_schema_options(schemas, value_prefix = '')
|
21
|
+
schemas.map do |c|
|
22
|
+
[c.humanized_name, "#{value_prefix}#{c.kind}"]
|
23
|
+
end.sort_by(&:first)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Pigeon
|
2
|
+
module TemplateHelper
|
3
|
+
def pigeon_render_template(template, &block)
|
4
|
+
renderer = Renderer::Base.new
|
5
|
+
renderer.delegate = block if block_given?
|
6
|
+
renderer.render template
|
7
|
+
end
|
8
|
+
|
9
|
+
def pigeon_render_channel_template(channel, template = nil, scope = 'channel', &block)
|
10
|
+
renderer = Renderer::ChannelRenderer.new channel, scope
|
11
|
+
renderer.delegate = block if block_given?
|
12
|
+
template ||= channel.schema.template
|
13
|
+
renderer.render template
|
14
|
+
end
|
15
|
+
|
16
|
+
def pigeon_render_channel(channel, scope = 'channel')
|
17
|
+
pigeon_render_channel_template(channel, nil, scope)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/app/models/.gitkeep
ADDED
File without changes
|
@@ -0,0 +1,220 @@
|
|
1
|
+
module Pigeon
|
2
|
+
class Channel
|
3
|
+
extend ActiveModel::Naming
|
4
|
+
extend ActiveModel::Translation
|
5
|
+
include ActiveModel::Conversion
|
6
|
+
include ActiveModel::Validations
|
7
|
+
include NestedScopes
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def type() nil end
|
11
|
+
|
12
|
+
def schemas
|
13
|
+
[]
|
14
|
+
end
|
15
|
+
|
16
|
+
def find_type(type)
|
17
|
+
begin
|
18
|
+
"Pigeon::#{type.to_s.capitalize}Channel".constantize
|
19
|
+
rescue
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def find_schema(kind)
|
25
|
+
schemas.find { |s| s.kind == kind }
|
26
|
+
end
|
27
|
+
|
28
|
+
def channel_accessor(*names)
|
29
|
+
options = names.extract_options!
|
30
|
+
|
31
|
+
names.each do |name|
|
32
|
+
reader, line = "def #{name}; attributes['#{name}']; end", __LINE__
|
33
|
+
writer, line = "def #{name}=(value); attributes['#{name}'] = value; end", __LINE__
|
34
|
+
|
35
|
+
class_eval reader, __FILE__, line unless options[:instance_reader] == false
|
36
|
+
class_eval writer, __FILE__, line unless options[:instance_writer] == false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def list
|
41
|
+
raise NotImplementedError
|
42
|
+
end
|
43
|
+
|
44
|
+
def find(*arguments)
|
45
|
+
scope = arguments.slice!(0)
|
46
|
+
options = arguments.slice!(0) || {}
|
47
|
+
|
48
|
+
case scope
|
49
|
+
when :all
|
50
|
+
list.map { |id| find_single(id) }
|
51
|
+
when :first
|
52
|
+
find_single(list.first)
|
53
|
+
when :last
|
54
|
+
find_single(list.first)
|
55
|
+
else
|
56
|
+
find_single(scope)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def all() find(:all) end
|
61
|
+
def first() find(:first) end
|
62
|
+
def last() find(:last) end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def find_single(id)
|
67
|
+
raise NotImplementedError
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
attr_reader :attributes
|
72
|
+
attr_accessor :schema
|
73
|
+
|
74
|
+
channel_accessor :name, :kind
|
75
|
+
|
76
|
+
validates_presence_of :name, :kind
|
77
|
+
|
78
|
+
def type
|
79
|
+
self.class.type
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(attrs = {}, persisted = false)
|
83
|
+
@errors = ChannelErrors.new(self)
|
84
|
+
@attributes = {}.with_indifferent_access
|
85
|
+
@persisted = persisted
|
86
|
+
@destroyed = false
|
87
|
+
|
88
|
+
attrs = attrs.dup
|
89
|
+
@schema = attrs.delete(:schema) || self.class.find_schema(attrs[:kind])
|
90
|
+
attrs[:kind] ||= @schema.kind unless @schema.nil?
|
91
|
+
|
92
|
+
load_default_values
|
93
|
+
load_schema_defaults
|
94
|
+
|
95
|
+
load(attrs)
|
96
|
+
end
|
97
|
+
|
98
|
+
def read_attribute_for_validation(key)
|
99
|
+
attributes[key]
|
100
|
+
end
|
101
|
+
|
102
|
+
def persisted?
|
103
|
+
@persisted
|
104
|
+
end
|
105
|
+
|
106
|
+
def new_record?
|
107
|
+
!persisted?
|
108
|
+
end
|
109
|
+
|
110
|
+
def destroyed?
|
111
|
+
@destroyed
|
112
|
+
end
|
113
|
+
|
114
|
+
alias :id :name
|
115
|
+
alias :id= :name=
|
116
|
+
|
117
|
+
def known_attributes
|
118
|
+
schema.try(:known_attributes) || []
|
119
|
+
end
|
120
|
+
|
121
|
+
def method_missing(method_symbol, *arguments)
|
122
|
+
method_name = method_symbol.to_s
|
123
|
+
|
124
|
+
if method_name =~ /(=|\?)$/
|
125
|
+
case $1
|
126
|
+
when "="
|
127
|
+
attributes[$`] = arguments.first
|
128
|
+
when "?"
|
129
|
+
attributes[$`]
|
130
|
+
end
|
131
|
+
else
|
132
|
+
return attributes[method_name] if attributes.include?(method_name)
|
133
|
+
# not set right now but we know about it
|
134
|
+
return nil if known_attributes.include?(method_name)
|
135
|
+
super
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def generate_name
|
140
|
+
"#{kind || 'channel'}-#{Time.now.strftime('%Y%m%d%H%M%S%3N')}"
|
141
|
+
end
|
142
|
+
|
143
|
+
def generate_name!
|
144
|
+
self.name = generate_name
|
145
|
+
end
|
146
|
+
|
147
|
+
def [](key)
|
148
|
+
attributes[key]
|
149
|
+
end
|
150
|
+
|
151
|
+
def []=(key, value)
|
152
|
+
attributes[key] = value
|
153
|
+
end
|
154
|
+
|
155
|
+
def read_attribute(attr_name)
|
156
|
+
find_attr_recursive(attr_name, attributes)
|
157
|
+
end
|
158
|
+
|
159
|
+
def write_attribute(attr_name, value)
|
160
|
+
hash, key = find_attr_ref_recursive(attr_name, attributes)
|
161
|
+
hash && hash[key] = value
|
162
|
+
end
|
163
|
+
|
164
|
+
def assign_attributes(new_attributes)
|
165
|
+
return if new_attributes.blank?
|
166
|
+
|
167
|
+
new_attributes = new_attributes.stringify_keys
|
168
|
+
new_attributes.each do |key, value|
|
169
|
+
if value.is_a?(Hash)
|
170
|
+
write_attribute(key, read_attribute(key).merge(value))
|
171
|
+
else
|
172
|
+
write_attribute(key, value)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def attributes=(new_attributes)
|
178
|
+
return unless new_attributes.is_a?(Hash)
|
179
|
+
assign_attributes(new_attributes)
|
180
|
+
end
|
181
|
+
|
182
|
+
def human_attribute_name(attr_name, options = {})
|
183
|
+
(!schema.nil? && schema.find_attribute(attr_name).try(:humanized_name)) ||
|
184
|
+
self.class.human_attribute_name(attr_name, options)
|
185
|
+
end
|
186
|
+
|
187
|
+
def save
|
188
|
+
raise NotImplementedError
|
189
|
+
end
|
190
|
+
|
191
|
+
def save!
|
192
|
+
save || raise(ChannelInvalid.new(self))
|
193
|
+
end
|
194
|
+
|
195
|
+
def destroy
|
196
|
+
raise NotImplementedError
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def load_default_values
|
202
|
+
end
|
203
|
+
|
204
|
+
def load_schema_defaults
|
205
|
+
load schema.default_values unless schema.nil?
|
206
|
+
end
|
207
|
+
|
208
|
+
def load(attributes)
|
209
|
+
(attributes || {}).each do |key, value|
|
210
|
+
@attributes[key.to_s] = case value
|
211
|
+
when Hash
|
212
|
+
(@attributes[key.to_s] || {}).merge(value)
|
213
|
+
else
|
214
|
+
value.duplicable? ? value.dup : value
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Pigeon
|
2
|
+
class ChannelAttribute < NestedAttribute
|
3
|
+
attr_reader :type, :default_value, :humanized_name, :label, :tooltip, :user_editable, :options
|
4
|
+
attr_writer :scope
|
5
|
+
|
6
|
+
def initialize(name, type, options = {})
|
7
|
+
raise ArgumentError, "name cannot be blank (scope was '#{options[:scope]}')" unless name.present?
|
8
|
+
raise ArgumentError, "invalid type #{type}" unless self.class.valid_type?(type)
|
9
|
+
|
10
|
+
@name = name
|
11
|
+
@type = type.to_sym
|
12
|
+
options = options.with_indifferent_access
|
13
|
+
|
14
|
+
@scope = options.delete(:scope)
|
15
|
+
|
16
|
+
@default_value = options['default_value'] || options['value']
|
17
|
+
@humanized_name = options['humanized_name'] || options['display']
|
18
|
+
@label = options['label']
|
19
|
+
if @label.present?
|
20
|
+
@humanized_name ||= @label
|
21
|
+
elsif @humanized_name.present?
|
22
|
+
@label ||= @humanized_name
|
23
|
+
else
|
24
|
+
@label = name.to_s.humanize
|
25
|
+
@humanized_name = @label
|
26
|
+
end
|
27
|
+
@tooltip = options['tooltip']
|
28
|
+
@user_editable = options['user'].nil? ? true : options['user']
|
29
|
+
@options = options['options'] || []
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.valid_type?(type)
|
33
|
+
%w(string boolean password enum integer timezone hidden).include?(type.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.build_default(name, hint_value = nil)
|
37
|
+
type = case hint_value
|
38
|
+
when Fixnum
|
39
|
+
:integer
|
40
|
+
when FalseClass, TrueClass
|
41
|
+
:boolean
|
42
|
+
else
|
43
|
+
:string
|
44
|
+
end
|
45
|
+
name, scope = build_scope_recursive(name)
|
46
|
+
new name, type, value: hint_value, scope: scope
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
extend NestedScopes
|
52
|
+
|
53
|
+
def self.build_scope_recursive(name, scope = nil)
|
54
|
+
fold_scoped_name(name, [nil, scope]) do |(current, scope), name|
|
55
|
+
if current.nil?
|
56
|
+
[name, scope]
|
57
|
+
else
|
58
|
+
[name, NestedAttribute.new(current, scope)]
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module Pigeon
|
2
|
+
class ChannelSchema
|
3
|
+
include NestedScopes
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def from_hash(type, hash)
|
7
|
+
self.new type, hash["kind"], hash["humanized_name"], hash["attributes"], hash["template"]
|
8
|
+
end
|
9
|
+
|
10
|
+
def list_from_hash(type, hash)
|
11
|
+
hash.map do |name, data|
|
12
|
+
data["kind"] ||= name
|
13
|
+
from_hash type, data
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :type, :kind, :humanized_name, :attribute_data, :template_data
|
19
|
+
|
20
|
+
def initialize(type, kind, humanized_name = nil, attributes = [], template = nil)
|
21
|
+
raise ArgumentError, "type cannot be blank" if type.blank?
|
22
|
+
raise ArgumentError, "kind cannot be blank" if kind.blank?
|
23
|
+
|
24
|
+
@type = type
|
25
|
+
@kind = kind
|
26
|
+
@humanized_name = humanized_name || kind
|
27
|
+
@attribute_data = attributes
|
28
|
+
@template_data = template
|
29
|
+
end
|
30
|
+
|
31
|
+
def attributes
|
32
|
+
@attributes ||= process_attributes(@attribute_data)
|
33
|
+
end
|
34
|
+
|
35
|
+
def known_attributes
|
36
|
+
attributes.keys
|
37
|
+
end
|
38
|
+
|
39
|
+
def user_attributes
|
40
|
+
flattened_map_filter(attributes) do |attr|
|
41
|
+
attr.user_editable ? attr.scoped_name : nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def default_values
|
46
|
+
recursive_map_filter(attributes, &:default_value)
|
47
|
+
end
|
48
|
+
|
49
|
+
def template
|
50
|
+
@template ||= @template_data ? process_template(@template_data) : default_template
|
51
|
+
end
|
52
|
+
|
53
|
+
def type_kind
|
54
|
+
"#{type}/#{kind}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def default_template
|
58
|
+
["@template"] + user_attributes.map do |attr_name|
|
59
|
+
["@attr", attr_name]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_attribute(name)
|
64
|
+
find_attr_recursive(name, attributes)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
# recursive map-filter of the channel schema attributes
|
70
|
+
def flattened_map_filter(attributes, &block)
|
71
|
+
attributes.inject([]) do |result, (name, attr)|
|
72
|
+
if attr.is_a? Hash
|
73
|
+
value = flattened_map_filter(attr, &block)
|
74
|
+
result + value
|
75
|
+
else
|
76
|
+
value = yield(attr)
|
77
|
+
result + [value].reject(&:nil?)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def recursive_map_filter(attributes, &block)
|
83
|
+
attributes.inject({}) do |result, (name, attr)|
|
84
|
+
if attr.is_a? Hash
|
85
|
+
value = recursive_map_filter(attr, &block)
|
86
|
+
else
|
87
|
+
value = yield(attr)
|
88
|
+
end
|
89
|
+
result[name] = value if value.present?
|
90
|
+
result
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def process_attributes(attributes, scope = nil)
|
95
|
+
Hash[attributes.map do |attr|
|
96
|
+
name = attr["name"]
|
97
|
+
if attr["attributes"].present?
|
98
|
+
# recursively process nested attributes
|
99
|
+
attribute = process_attributes(attr["attributes"], NestedAttribute.new(name, scope))
|
100
|
+
else
|
101
|
+
type = attr["type"] || "string"
|
102
|
+
attribute = Pigeon::ChannelAttribute.new(name, type, attr)
|
103
|
+
attribute.scope = scope
|
104
|
+
end
|
105
|
+
[name, attribute]
|
106
|
+
end].with_indifferent_access
|
107
|
+
end
|
108
|
+
|
109
|
+
def process_template(template)
|
110
|
+
template
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|