instedd-pigeon 0.1.1
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/.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
|