open_rosa 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/CHANGELOG.md +25 -0
- data/CLAUDE.md +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +285 -0
- data/Rakefile +12 -0
- data/examples/authentication.rb +80 -0
- data/examples/rack_app.rb +178 -0
- data/examples/rails_integration.rb +152 -0
- data/lib/open_rosa/field_context.rb +59 -0
- data/lib/open_rosa/fields/base.rb +18 -0
- data/lib/open_rosa/fields/boolean.rb +17 -0
- data/lib/open_rosa/fields/group.rb +19 -0
- data/lib/open_rosa/fields/input.rb +32 -0
- data/lib/open_rosa/fields/range.rb +34 -0
- data/lib/open_rosa/fields/repeat.rb +20 -0
- data/lib/open_rosa/fields/select.rb +28 -0
- data/lib/open_rosa/fields/select1.rb +27 -0
- data/lib/open_rosa/fields/trigger.rb +17 -0
- data/lib/open_rosa/fields/upload.rb +18 -0
- data/lib/open_rosa/form.rb +146 -0
- data/lib/open_rosa/form_dsl.rb +70 -0
- data/lib/open_rosa/form_list.rb +97 -0
- data/lib/open_rosa/manifest.rb +105 -0
- data/lib/open_rosa/media_file.rb +97 -0
- data/lib/open_rosa/middleware.rb +352 -0
- data/lib/open_rosa/submission.rb +195 -0
- data/lib/open_rosa/version.rb +5 -0
- data/lib/open_rosa/xform.rb +250 -0
- data/lib/open_rosa.rb +27 -0
- data/sig/openrosa.rbs +4 -0
- metadata +105 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rack"
|
|
4
|
+
require "nokogiri"
|
|
5
|
+
|
|
6
|
+
module OpenRosa
|
|
7
|
+
# Represents a parsed OpenRosa form submission
|
|
8
|
+
#
|
|
9
|
+
# Handles parsing of multipart MIME submissions containing:
|
|
10
|
+
# - XML submission file (required, named "xml_submission_file")
|
|
11
|
+
# - Media attachments (optional, images/audio/video)
|
|
12
|
+
#
|
|
13
|
+
# Per OpenRosa spec:
|
|
14
|
+
# - POST to /submission with multipart/form-data
|
|
15
|
+
# - XML part must be named "xml_submission_file"
|
|
16
|
+
# - Other parts are treated as media attachments
|
|
17
|
+
class Submission # rubocop:disable Metrics/ClassLength
|
|
18
|
+
attr_reader :form_id, :instance_id, :data, :attachments, :metadata, :raw_xml
|
|
19
|
+
|
|
20
|
+
class ParseError < StandardError; end
|
|
21
|
+
|
|
22
|
+
# rubocop:disable Metrics/ParameterLists
|
|
23
|
+
def initialize(form_id:, instance_id:, data:, raw_xml:, attachments: [], metadata: {})
|
|
24
|
+
# rubocop:enable Metrics/ParameterLists
|
|
25
|
+
@form_id = form_id
|
|
26
|
+
@instance_id = instance_id
|
|
27
|
+
@data = data
|
|
28
|
+
@attachments = attachments
|
|
29
|
+
@metadata = metadata
|
|
30
|
+
@raw_xml = raw_xml
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# Parse a Rack request into a Submission object
|
|
35
|
+
#
|
|
36
|
+
# @param rack_request [Rack::Request] The incoming request
|
|
37
|
+
# @return [Submission] Parsed submission
|
|
38
|
+
# @raise [ParseError] If submission is invalid
|
|
39
|
+
def parse(rack_request)
|
|
40
|
+
validate_request!(rack_request)
|
|
41
|
+
raw_xml = extract_raw_xml(rack_request)
|
|
42
|
+
doc = parse_xml(raw_xml)
|
|
43
|
+
|
|
44
|
+
build_submission(doc, rack_request, raw_xml)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def validate_request!(rack_request)
|
|
50
|
+
raise ParseError, "Request must be POST" unless rack_request.post?
|
|
51
|
+
raise ParseError, "Content-Type must be multipart/form-data" unless multipart?(rack_request)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def extract_raw_xml(rack_request)
|
|
55
|
+
xml_param = rack_request.params["xml_submission_file"]
|
|
56
|
+
raise ParseError, "Missing xml_submission_file parameter" unless xml_param
|
|
57
|
+
|
|
58
|
+
raw_xml = read_xml_param(xml_param)
|
|
59
|
+
raise ParseError, "Empty xml_submission_file" if raw_xml.nil? || raw_xml.empty?
|
|
60
|
+
|
|
61
|
+
raw_xml
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def read_xml_param(xml_param)
|
|
65
|
+
if xml_param.respond_to?(:read)
|
|
66
|
+
xml_param.read
|
|
67
|
+
elsif xml_param.is_a?(Hash) && xml_param[:tempfile]
|
|
68
|
+
xml_param[:tempfile].read
|
|
69
|
+
else
|
|
70
|
+
xml_param.to_s
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def build_submission(doc, rack_request, raw_xml)
|
|
75
|
+
new(
|
|
76
|
+
form_id: extract_form_id(doc),
|
|
77
|
+
instance_id: extract_instance_id(doc),
|
|
78
|
+
data: extract_data(doc),
|
|
79
|
+
attachments: extract_attachments(rack_request),
|
|
80
|
+
metadata: extract_metadata(doc),
|
|
81
|
+
raw_xml: raw_xml
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def multipart?(request)
|
|
86
|
+
content_type = request.content_type
|
|
87
|
+
content_type&.start_with?("multipart/form-data")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def parse_xml(raw_xml)
|
|
91
|
+
Nokogiri::XML(raw_xml) do |config|
|
|
92
|
+
config.strict.nonet
|
|
93
|
+
end
|
|
94
|
+
rescue Nokogiri::XML::SyntaxError => e
|
|
95
|
+
raise ParseError, "Invalid XML: #{e.message}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def extract_form_id(doc)
|
|
99
|
+
# The root element's name or id attribute is the form ID
|
|
100
|
+
root = doc.root
|
|
101
|
+
raise ParseError, "XML document has no root element" unless root
|
|
102
|
+
|
|
103
|
+
# Try id attribute first, then use element name
|
|
104
|
+
root["id"] || root.name
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def extract_instance_id(doc)
|
|
108
|
+
# Look for meta/instanceID element
|
|
109
|
+
instance_id_node = doc.at_xpath("//meta/instanceID") ||
|
|
110
|
+
doc.at_xpath("//*[local-name()='instanceID']")
|
|
111
|
+
|
|
112
|
+
instance_id_node&.text&.strip
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def extract_data(doc)
|
|
116
|
+
root = doc.root
|
|
117
|
+
data = {}
|
|
118
|
+
|
|
119
|
+
# Recursively extract all leaf nodes as data
|
|
120
|
+
extract_node_data(root, data)
|
|
121
|
+
|
|
122
|
+
data
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def extract_node_data(node, data, prefix = nil)
|
|
126
|
+
node.element_children.each do |child|
|
|
127
|
+
# Skip meta elements
|
|
128
|
+
next if child.name == "meta"
|
|
129
|
+
|
|
130
|
+
field_name = prefix ? "#{prefix}/#{child.name}" : child.name
|
|
131
|
+
|
|
132
|
+
if child.element_children.empty?
|
|
133
|
+
# Leaf node - extract value
|
|
134
|
+
data[field_name] = child.text.strip
|
|
135
|
+
else
|
|
136
|
+
# Has children - recurse (for groups/repeats)
|
|
137
|
+
extract_node_data(child, data, field_name)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def extract_metadata(doc)
|
|
143
|
+
metadata = {}
|
|
144
|
+
|
|
145
|
+
# Extract common metadata fields
|
|
146
|
+
meta_node = doc.at_xpath("//meta") || doc.at_xpath("//*[local-name()='meta']")
|
|
147
|
+
return metadata unless meta_node
|
|
148
|
+
|
|
149
|
+
["instanceID", "timeStart", "timeEnd", "deviceID", "userID"].each do |field|
|
|
150
|
+
node = meta_node.at_xpath(field) || meta_node.at_xpath("*[local-name()='#{field}']")
|
|
151
|
+
metadata[field.to_sym] = node.text.strip if node
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
metadata
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def extract_attachments(rack_request)
|
|
158
|
+
rack_request.params.filter_map do |name, value|
|
|
159
|
+
next if name == "xml_submission_file"
|
|
160
|
+
next unless value.is_a?(Hash) && value[:tempfile]
|
|
161
|
+
|
|
162
|
+
Attachment.new(
|
|
163
|
+
filename: value[:filename],
|
|
164
|
+
content_type: value[:type] || "application/octet-stream",
|
|
165
|
+
size: value[:tempfile].size,
|
|
166
|
+
tempfile: value[:tempfile]
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Represents an uploaded file attachment
|
|
173
|
+
class Attachment
|
|
174
|
+
attr_reader :filename, :content_type, :size, :tempfile
|
|
175
|
+
|
|
176
|
+
def initialize(filename:, content_type:, size:, tempfile:)
|
|
177
|
+
@filename = filename
|
|
178
|
+
@content_type = content_type
|
|
179
|
+
@size = size
|
|
180
|
+
@tempfile = tempfile
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Read the file contents
|
|
184
|
+
def read
|
|
185
|
+
tempfile.rewind
|
|
186
|
+
tempfile.read
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Get the file path (useful for moving/copying)
|
|
190
|
+
def path
|
|
191
|
+
tempfile.path
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "nokogiri"
|
|
4
|
+
|
|
5
|
+
module OpenRosa
|
|
6
|
+
# Generates XForm XML from Form definitions
|
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
|
8
|
+
class XForm
|
|
9
|
+
XFORMS_NS = "http://www.w3.org/2002/xforms"
|
|
10
|
+
XHTML_NS = "http://www.w3.org/1999/xhtml"
|
|
11
|
+
|
|
12
|
+
def initialize(form_class)
|
|
13
|
+
@form_class = form_class
|
|
14
|
+
@form_id = form_class.form_id
|
|
15
|
+
@version = form_class.version
|
|
16
|
+
@name = form_class.name || form_class.form_id
|
|
17
|
+
@fields = form_class.fields
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_xml
|
|
21
|
+
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
|
|
22
|
+
xml.html(xform_namespaces) do
|
|
23
|
+
generate_head(xml)
|
|
24
|
+
generate_body(xml)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
builder.to_xml
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def xform_namespaces
|
|
32
|
+
{
|
|
33
|
+
"xmlns" => XFORMS_NS,
|
|
34
|
+
"xmlns:h" => XHTML_NS,
|
|
35
|
+
"xmlns:ev" => "http://www.w3.org/2001/xml-events",
|
|
36
|
+
"xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
|
|
37
|
+
"xmlns:jr" => "http://openrosa.org/javarosa"
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def generate_head(xml)
|
|
44
|
+
xml.head do
|
|
45
|
+
xml.title(@name, xmlns: XHTML_NS)
|
|
46
|
+
xml.model do
|
|
47
|
+
generate_instance(xml)
|
|
48
|
+
generate_bindings(xml)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def generate_instance(xml)
|
|
54
|
+
xml.instance do
|
|
55
|
+
xml.send(@form_id.to_sym, id: @form_id, version: @version) do
|
|
56
|
+
generate_instance_fields(xml, @fields)
|
|
57
|
+
generate_metadata(xml)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def generate_instance_fields(xml, fields, _path_prefix = nil)
|
|
63
|
+
fields.each do |field|
|
|
64
|
+
case field
|
|
65
|
+
when Fields::Group, Fields::Repeat
|
|
66
|
+
xml.send(field.name.to_sym) do
|
|
67
|
+
generate_instance_fields(xml, field.fields, field.name)
|
|
68
|
+
end
|
|
69
|
+
else
|
|
70
|
+
xml.send(field.name.to_sym)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def generate_metadata(xml)
|
|
76
|
+
xml.meta do
|
|
77
|
+
xml.instanceID
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def generate_bindings(xml)
|
|
82
|
+
generate_field_bindings(xml, @fields, "/#{@form_id}")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def generate_field_bindings(xml, fields, path_prefix)
|
|
86
|
+
fields.each do |field|
|
|
87
|
+
nodeset = "#{path_prefix}/#{field.name}"
|
|
88
|
+
generate_binding_for_field(xml, field, nodeset)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def generate_binding_for_field(xml, field, nodeset)
|
|
93
|
+
case field
|
|
94
|
+
when Fields::Group
|
|
95
|
+
xml.bind(nodeset: nodeset, relevant: field.relevant) if field.relevant
|
|
96
|
+
generate_field_bindings(xml, field.fields, nodeset)
|
|
97
|
+
when Fields::Repeat
|
|
98
|
+
generate_field_bindings(xml, field.fields, nodeset)
|
|
99
|
+
else
|
|
100
|
+
xml.bind(build_bind_attrs(field, nodeset))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_bind_attrs(field, nodeset)
|
|
105
|
+
attrs = { nodeset: nodeset, type: binding_type(field) }
|
|
106
|
+
attrs[:required] = "true()" if field.required
|
|
107
|
+
attrs[:constraint] = field.constraint if field.respond_to?(:constraint) && field.constraint
|
|
108
|
+
attrs
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def binding_type(field)
|
|
112
|
+
case field
|
|
113
|
+
when Fields::Input, Fields::Range
|
|
114
|
+
field.type.to_s
|
|
115
|
+
when Fields::Upload
|
|
116
|
+
"binary"
|
|
117
|
+
else
|
|
118
|
+
"string"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def generate_body(xml)
|
|
123
|
+
xml.body(xmlns: XHTML_NS) do
|
|
124
|
+
generate_controls(xml, @fields, "/#{@form_id}")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def generate_controls(xml, fields, path_prefix)
|
|
129
|
+
fields.each do |field|
|
|
130
|
+
ref = "#{path_prefix}/#{field.name}"
|
|
131
|
+
generate_control_for_field(xml, field, ref)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
136
|
+
def generate_control_for_field(xml, field, ref)
|
|
137
|
+
case field
|
|
138
|
+
when Fields::Input then generate_input_control(xml, field, ref)
|
|
139
|
+
when Fields::Select1 then generate_select1_control(xml, field, ref)
|
|
140
|
+
when Fields::Select then generate_select_control(xml, field, ref)
|
|
141
|
+
when Fields::Boolean then generate_boolean_control(xml, field, ref)
|
|
142
|
+
when Fields::Upload then generate_upload_control(xml, field, ref)
|
|
143
|
+
when Fields::Range then generate_range_control(xml, field, ref)
|
|
144
|
+
when Fields::Trigger then generate_trigger_control(xml, field, ref)
|
|
145
|
+
when Fields::Group then generate_group_control(xml, field, ref)
|
|
146
|
+
when Fields::Repeat then generate_repeat_control(xml, field, ref)
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
150
|
+
|
|
151
|
+
def generate_input_control(xml, field, ref)
|
|
152
|
+
xml.input(ref: ref) do
|
|
153
|
+
add_label_and_hint(xml, field)
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def generate_select1_control(xml, field, ref)
|
|
158
|
+
xml.select1(ref: ref) do
|
|
159
|
+
add_label_and_hint(xml, field)
|
|
160
|
+
generate_choices(xml, field.choices)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def generate_select_control(xml, field, ref)
|
|
165
|
+
xml.select(ref: ref) do
|
|
166
|
+
add_label_and_hint(xml, field)
|
|
167
|
+
generate_choices(xml, field.choices)
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def generate_boolean_control(xml, field, ref)
|
|
172
|
+
xml.select1(ref: ref) do
|
|
173
|
+
add_label_and_hint(xml, field)
|
|
174
|
+
generate_boolean_choices(xml)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def generate_boolean_choices(xml)
|
|
179
|
+
xml.item do
|
|
180
|
+
xml.label "Yes"
|
|
181
|
+
xml.value "true"
|
|
182
|
+
end
|
|
183
|
+
xml.item do
|
|
184
|
+
xml.label "No"
|
|
185
|
+
xml.value "false"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def generate_upload_control(xml, field, ref)
|
|
190
|
+
xml.upload(ref: ref, mediatype: field.mediatype) do
|
|
191
|
+
add_label_and_hint(xml, field)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def generate_range_control(xml, field, ref)
|
|
196
|
+
xml.range(ref: ref, start: field.start, end: field.end, step: field.step) do
|
|
197
|
+
add_label_and_hint(xml, field)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def generate_trigger_control(xml, field, ref)
|
|
202
|
+
xml.trigger(ref: ref) do
|
|
203
|
+
add_label_and_hint(xml, field)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def generate_group_control(xml, field, ref)
|
|
208
|
+
xml.group(ref: ref) do
|
|
209
|
+
xml.label field.label if field.label
|
|
210
|
+
generate_controls(xml, field.fields, ref)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def generate_repeat_control(xml, field, ref)
|
|
215
|
+
xml.repeat(nodeset: ref) do
|
|
216
|
+
xml.label field.label if field.label
|
|
217
|
+
generate_controls(xml, field.fields, ref)
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def add_label_and_hint(xml, field)
|
|
222
|
+
xml.label field.label if field.label
|
|
223
|
+
xml.hint field.hint if field.hint
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def generate_choices(xml, choices)
|
|
227
|
+
generate_array_choices(xml, choices) if choices.is_a?(Array)
|
|
228
|
+
generate_hash_choices(xml, choices) if choices.is_a?(Hash)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def generate_array_choices(xml, choices)
|
|
232
|
+
choices.each do |choice|
|
|
233
|
+
xml.item do
|
|
234
|
+
xml.label choice
|
|
235
|
+
xml.value choice
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def generate_hash_choices(xml, choices)
|
|
241
|
+
choices.each do |label, value|
|
|
242
|
+
xml.item do
|
|
243
|
+
xml.label label
|
|
244
|
+
xml.value value
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
# rubocop:enable Metrics/ClassLength
|
|
250
|
+
end
|
data/lib/open_rosa.rb
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "open_rosa/version"
|
|
4
|
+
require_relative "open_rosa/fields/base"
|
|
5
|
+
require_relative "open_rosa/fields/input"
|
|
6
|
+
require_relative "open_rosa/fields/select1"
|
|
7
|
+
require_relative "open_rosa/fields/select"
|
|
8
|
+
require_relative "open_rosa/fields/upload"
|
|
9
|
+
require_relative "open_rosa/fields/range"
|
|
10
|
+
require_relative "open_rosa/fields/trigger"
|
|
11
|
+
require_relative "open_rosa/fields/boolean"
|
|
12
|
+
require_relative "open_rosa/fields/group"
|
|
13
|
+
require_relative "open_rosa/fields/repeat"
|
|
14
|
+
require_relative "open_rosa/field_context"
|
|
15
|
+
require_relative "open_rosa/form_dsl"
|
|
16
|
+
require_relative "open_rosa/form"
|
|
17
|
+
require_relative "open_rosa/xform"
|
|
18
|
+
require_relative "open_rosa/form_list"
|
|
19
|
+
require_relative "open_rosa/submission"
|
|
20
|
+
require_relative "open_rosa/media_file"
|
|
21
|
+
require_relative "open_rosa/manifest"
|
|
22
|
+
require_relative "open_rosa/middleware"
|
|
23
|
+
|
|
24
|
+
module OpenRosa
|
|
25
|
+
class Error < StandardError; end
|
|
26
|
+
# Your code goes here...
|
|
27
|
+
end
|
data/sig/openrosa.rbs
ADDED
metadata
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: open_rosa
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Alex Watt
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-10 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: nokogiri
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.15'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.15'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rack
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '3.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '3.0'
|
|
41
|
+
description:
|
|
42
|
+
email:
|
|
43
|
+
- alex@alexcwatt.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- CHANGELOG.md
|
|
49
|
+
- CLAUDE.md
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- Rakefile
|
|
53
|
+
- examples/authentication.rb
|
|
54
|
+
- examples/rack_app.rb
|
|
55
|
+
- examples/rails_integration.rb
|
|
56
|
+
- lib/open_rosa.rb
|
|
57
|
+
- lib/open_rosa/field_context.rb
|
|
58
|
+
- lib/open_rosa/fields/base.rb
|
|
59
|
+
- lib/open_rosa/fields/boolean.rb
|
|
60
|
+
- lib/open_rosa/fields/group.rb
|
|
61
|
+
- lib/open_rosa/fields/input.rb
|
|
62
|
+
- lib/open_rosa/fields/range.rb
|
|
63
|
+
- lib/open_rosa/fields/repeat.rb
|
|
64
|
+
- lib/open_rosa/fields/select.rb
|
|
65
|
+
- lib/open_rosa/fields/select1.rb
|
|
66
|
+
- lib/open_rosa/fields/trigger.rb
|
|
67
|
+
- lib/open_rosa/fields/upload.rb
|
|
68
|
+
- lib/open_rosa/form.rb
|
|
69
|
+
- lib/open_rosa/form_dsl.rb
|
|
70
|
+
- lib/open_rosa/form_list.rb
|
|
71
|
+
- lib/open_rosa/manifest.rb
|
|
72
|
+
- lib/open_rosa/media_file.rb
|
|
73
|
+
- lib/open_rosa/middleware.rb
|
|
74
|
+
- lib/open_rosa/submission.rb
|
|
75
|
+
- lib/open_rosa/version.rb
|
|
76
|
+
- lib/open_rosa/xform.rb
|
|
77
|
+
- sig/openrosa.rbs
|
|
78
|
+
homepage: https://github.com/alexcwatt/openrosa-ruby
|
|
79
|
+
licenses:
|
|
80
|
+
- MIT
|
|
81
|
+
metadata:
|
|
82
|
+
homepage_uri: https://github.com/alexcwatt/openrosa-ruby
|
|
83
|
+
source_code_uri: https://github.com/alexcwatt/openrosa-ruby
|
|
84
|
+
changelog_uri: https://github.com/alexcwatt/openrosa-ruby/blob/main/CHANGELOG.md
|
|
85
|
+
rubygems_mfa_required: 'true'
|
|
86
|
+
post_install_message:
|
|
87
|
+
rdoc_options: []
|
|
88
|
+
require_paths:
|
|
89
|
+
- lib
|
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
91
|
+
requirements:
|
|
92
|
+
- - ">="
|
|
93
|
+
- !ruby/object:Gem::Version
|
|
94
|
+
version: 3.2.0
|
|
95
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
96
|
+
requirements:
|
|
97
|
+
- - ">="
|
|
98
|
+
- !ruby/object:Gem::Version
|
|
99
|
+
version: '0'
|
|
100
|
+
requirements: []
|
|
101
|
+
rubygems_version: 3.5.23
|
|
102
|
+
signing_key:
|
|
103
|
+
specification_version: 4
|
|
104
|
+
summary: Manage OpenRosa forms.
|
|
105
|
+
test_files: []
|