frecon 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f815f2ffe36eae99a451ce057c3f2bc41a355783
4
+ data.tar.gz: 0b415f99c4fdc44bc23236bd259cddec01bd505b
5
+ SHA512:
6
+ metadata.gz: 86b2d39602ae88bce2c4c10312ac8f9f91f29170b8774104e954d7300bb6e56cecf97ee3d72ec6c5c7c656b7c9c100227efa72cf5df74849a1f944b176c13fb7
7
+ data.tar.gz: 0ce13da7ae08cffc748ad6e4ca3baa30e8a0606dfc98b5bc4131b7f59a2a24edc730540a028a2797ae780aed853d03fa87e9b108878465f1eac37a63611b7a89
data/bin/frecon ADDED
@@ -0,0 +1,13 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ # Allow running locally.
4
+ lib_directory = File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
5
+ $LOAD_PATH.unshift(lib_directory) unless $LOAD_PATH.include?(lib_directory)
6
+
7
+ require "frecon"
8
+
9
+ unless ARGV.include?("c") || ARGV.include?("console")
10
+ FReCon::Server.start
11
+ else
12
+ FReCon::Console.start
13
+ end
@@ -0,0 +1,3 @@
1
+ class Object
2
+ alias_method :is_an?, :is_a?
3
+ end
@@ -0,0 +1,4 @@
1
+ module FReCon
2
+ ENVIRONMENT = :development
3
+ VERSION = "0.1.0"
4
+ end
@@ -0,0 +1,2 @@
1
+ require "frecon/base/object"
2
+ require "frecon/base/variables"
@@ -0,0 +1,28 @@
1
+ require "frecon/database"
2
+ require "frecon/server"
3
+
4
+ module FReCon
5
+ class Console
6
+ def self.start
7
+ Database.setup
8
+
9
+ # Use pry if it is installed.
10
+ # Use the context of the FReCon module;
11
+ # this allows for writing "Team" instead of "FReCon::Team".
12
+ begin
13
+ require "pry"
14
+
15
+ FReCon.pry
16
+ rescue LoadError
17
+ require "irb"
18
+
19
+ IRB.setup nil
20
+ IRB.conf[:MAIN_CONTEXT] = IRB::Irb.new.context
21
+
22
+ require "irb/ext/multi-irb"
23
+
24
+ IRB.irb nil, FReCon
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,129 @@
1
+ require "frecon/base"
2
+
3
+ module FReCon
4
+ class Controller
5
+ def self.model_name
6
+ # Removes the namespace "FReCon::" and "Controller" from
7
+ # the class name, then singularizes the result.
8
+ self.name.gsub(/FReCon::|Controller\Z/, "").singularize
9
+ end
10
+
11
+ def self.model
12
+ # Removes the trailing "Controller" from the class name,
13
+ # singularizes the result, and turns it into the class.
14
+ self.name.gsub(/Controller\Z/, "").singularize.constantize
15
+ end
16
+
17
+ # The 404 error message.
18
+ def self.could_not_find(value, attribute = "id", model = model_name.downcase)
19
+ "Could not find #{model_name.downcase} of #{attribute} #{value}!"
20
+ end
21
+
22
+ # Processes a POST/PUT request and returns the post data.
23
+ def self.process_request(request)
24
+ # Rewind the request body (an IO object)
25
+ # in case someone else has already played
26
+ # through it.
27
+ request.body.rewind
28
+
29
+ begin
30
+ # Parse the POST data as a JSON hash
31
+ # (because that's what it is)
32
+ post_data = JSON.parse(request.body.read)
33
+ rescue JSON::ParserError => e
34
+ # If we have malformed JSON (JSON::ParserError is
35
+ # raised), escape out of the function.
36
+ return [400, ErrorFormatter.format(e.message)]
37
+ end
38
+
39
+ post_data.is_an?(Array) ? [422, ErrorFormatter.format("Must pass a JSON object!")] : post_data
40
+ end
41
+
42
+ def self.create(request, params)
43
+ post_data = process_request request
44
+ return post_data if post_data.is_an?(Array)
45
+
46
+ @model = model.new
47
+ @model.attributes = post_data
48
+
49
+ if @model.save
50
+ [201, @model.to_json]
51
+ else
52
+ [422, ErrorFormatter.format(@model.errors.full_messages)]
53
+ end
54
+ end
55
+
56
+ def self.update(request, params)
57
+ return [400, "Must supply a #{model_name.downcase}!"] unless params[:id]
58
+
59
+ post_data = process_request request
60
+ return post_data if post_data.is_an?(Array)
61
+
62
+ @model = model.find params[:id]
63
+
64
+ return [404, ErrorFormatter.format(could_not_find(params[:id]))] unless @model
65
+
66
+ if @model.update_attributes(post_data)
67
+ @model.to_json
68
+ else
69
+ [422, ErrorFormatter.format(@model.errors.full_messages)]
70
+ end
71
+ end
72
+
73
+ def self.delete(params)
74
+ @model = model.find params[:id]
75
+
76
+ if @model
77
+ if @model.destroy
78
+ 204
79
+ else
80
+ [422, ErrorFormatter.format(@model.errors.full_messages)]
81
+ end
82
+ else
83
+ [404, ErrorFormatter.format(could_not_find(params[:id]))]
84
+ end
85
+ end
86
+
87
+ def self.show(params)
88
+ @model = model.find params[:id]
89
+
90
+ if @model
91
+ @model.to_json
92
+ else
93
+ [404, ErrorFormatter.format(could_not_find(params[:id]))]
94
+ end
95
+ end
96
+
97
+ def self.index(params)
98
+ params.delete("_")
99
+
100
+ @models = params.empty? ? model.all : model.where(params)
101
+
102
+ @models.to_json
103
+ end
104
+
105
+ def self.show_attribute(params, attribute)
106
+ @model = model.find params[:id]
107
+
108
+ if @model
109
+ @model.send(attribute).to_json
110
+ else
111
+ [404, ErrorFormatter.format(could_not_find(params[:id]))]
112
+ end
113
+ end
114
+
115
+ def self.team_number_to_team_id(post_data)
116
+ if post_data["team_number"] && !post_data["team_id"]
117
+ unless (team = Team.number post_data["team_number"]).nil?
118
+ post_data["team_id"] = team.id
119
+
120
+ post_data.delete("team_number")
121
+
122
+ post_data
123
+ else
124
+ return [404, ErrorFormatter.format(could_not_find(post_data["team_number"], "number", "team"))]
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,18 @@
1
+ require "json"
2
+ require "frecon/models"
3
+
4
+ module FReCon
5
+ class CompetitionsController < Controller
6
+ def self.teams(params)
7
+ show_attribute params, :teams
8
+ end
9
+
10
+ def self.matches(params)
11
+ show_attribute params, :matches
12
+ end
13
+
14
+ def self.records(params)
15
+ show_attribute params, :records
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ require "json"
2
+ require "frecon/models"
3
+
4
+ module FReCon
5
+ class DumpController
6
+ def self.full(params)
7
+ competitions = Competition.all
8
+ teams = Team.all
9
+ matches = Match.all
10
+ records = Record.all
11
+
12
+ {
13
+ "competitions" => competitions,
14
+ "teams" => teams,
15
+ "matches" => matches,
16
+ "records" => records
17
+ }.to_json
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,14 @@
1
+ require "json"
2
+ require "frecon/models"
3
+
4
+ module FReCon
5
+ class MatchesController < Controller
6
+ def self.competition(params)
7
+ show_attribute params, :competition
8
+ end
9
+
10
+ def self.records(params)
11
+ show_attribute params, :records
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,28 @@
1
+ module FReCon
2
+ class ParticipationsController < Controller
3
+ def self.create(request, params)
4
+ post_data = process_request request
5
+ return post_data if post_data.is_an?(Array)
6
+
7
+ # Convert team number to team_id.
8
+ post_data = team_number_to_team_id(post_data)
9
+
10
+ @model = model.new
11
+ @model.attributes = post_data
12
+
13
+ if @model.save
14
+ [201, @model.to_json]
15
+ else
16
+ [422, ErrorFormatter.format(@model.errors.full_messages)]
17
+ end
18
+ end
19
+
20
+ def self.competition(params)
21
+ show_attribute params, :competition
22
+ end
23
+
24
+ def self.team(params)
25
+ show_attribute params, :team
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,59 @@
1
+ require "json"
2
+ require "frecon/base"
3
+ require "frecon/models"
4
+
5
+ module FReCon
6
+ class RecordsController < Controller
7
+ def self.create(request, params)
8
+ post_data = process_request request
9
+ return post_data if post_data.is_an?(Array)
10
+
11
+ # Change special post_data attributes.
12
+ # Convert team number to team id.
13
+ post_data = team_number_to_team_id(post_data)
14
+
15
+ # Convert match number and competition name to match id.
16
+ if post_data["match_number"] && !post_data["match_id"]
17
+ if post_data["competition_name"] && (competition = Competition.find_by name: post_data["competition_name"])
18
+ # Try to set the match to the already existing match.
19
+ match = competition.matches.find_by number: post_data["match_number"]
20
+
21
+ # Create the match if necessary.
22
+ match ||= Match.create(number: post_data["match_number"], competition_id: competition.id)
23
+
24
+ post_data["match_id"] = match.id
25
+
26
+ post_data.delete("match_number")
27
+ post_data.delete("competition_name")
28
+ elsif post_data["competition"] && post_data["competition"]["_id"] && post_data["competition"]["_id"]["$oid"] && (competition = Competition.find_by(id: post_data["competition"]["_id"]["$oid"]))
29
+ # Try to set the match to the already existing match.
30
+ match = competition.matches.find_by number: post_data["match_number"]
31
+
32
+ # Create the match if necessary.
33
+ match ||= Match.create(number: post_data["match_number"], competition_id: competition.id)
34
+
35
+ post_data["match_id"] = match.id
36
+
37
+ post_data.delete("match_number")
38
+ post_data.delete("competition")
39
+ else
40
+ return [422, ErrorFormatter.format("A current competition is not set. Please set it.")]
41
+ end
42
+ end
43
+
44
+ @record = Record.new
45
+ @record.attributes = post_data
46
+
47
+ if @record.save
48
+ # Use to_json for now; we can filter it later.
49
+ [201, @record.to_json]
50
+ else
51
+ [422, ErrorFormatter.format(@record.errors.full_messages)]
52
+ end
53
+ end
54
+
55
+ def self.competition(params)
56
+ show_attribute params, :competition
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,14 @@
1
+ require "json"
2
+ require "frecon/models/robot"
3
+
4
+ module FReCon
5
+ class RobotsController < Controller
6
+ def self.competition(params)
7
+ show_attribute params, :competition
8
+ end
9
+
10
+ def self.team(params)
11
+ show_attribute params, :team
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,112 @@
1
+ require "json"
2
+ require "frecon/base"
3
+ require "frecon/models"
4
+
5
+ module FReCon
6
+ class TeamsController < Controller
7
+ def self.create(request, params)
8
+ post_data = process_request request
9
+ return post_data if post_data.is_an?(Array)
10
+
11
+ @team = Team.new
12
+ @team.attributes = post_data
13
+
14
+ if @team.save
15
+ # Use to_json for now; we can filter it later.
16
+ [201, @team.to_json]
17
+ else
18
+ [422, ErrorFormatter.format(@team.errors.full_messages)]
19
+ end
20
+ end
21
+
22
+ def self.update(request, params)
23
+ return [400, "Must supply a team number!"] unless params[:number]
24
+
25
+ post_data = process_request request
26
+ return post_data if post_data.is_an?(Array)
27
+
28
+ @team = Team.find_by number: params[:number]
29
+
30
+ if @team.nil?
31
+ return [404, ErrorFormatter.format(could_not_find(params[:number], "number"))]
32
+ end
33
+
34
+ if @team.update_attributes(post_data)
35
+ @team.to_json
36
+ else
37
+ [422, ErrorFormatter.format(@team.errors.full_messages)]
38
+ end
39
+ end
40
+
41
+ def self.delete(params)
42
+ @team = Team.find_by number: params[:number]
43
+
44
+ if @team
45
+ if @team.destroy
46
+ 204
47
+ else
48
+ [422, ErrorFormatter.format(@team.errors.full_messages)]
49
+ end
50
+ else
51
+ [404, ErrorFormatter.format(could_not_find(params[:number], "number"))]
52
+ end
53
+ end
54
+
55
+ def self.show(params)
56
+ @team = Team.find_by number: params[:number]
57
+
58
+ if @team
59
+ @team.to_json
60
+ else
61
+ [404, ErrorFormatter.format(could_not_find(params[:number], "number"))]
62
+ end
63
+ end
64
+
65
+ def self.records(params)
66
+ @team = Team.find_by number: params[:number]
67
+
68
+ if @team
69
+ if params[:competition_id]
70
+ @competition = Competition.find params[:competition_id]
71
+
72
+ if @competition
73
+ @team.records.in(match_id: @competition.matches.map { |match| match.id }).to_json
74
+ else
75
+ [404, ErrorFormatter.format(could_not_find(params[:competition_id], "id", "competition"))]
76
+ end
77
+ else
78
+ @team.records.to_json
79
+ end
80
+ else
81
+ [404, ErrorFormatter.format(could_not_find(params[:number], "number"))]
82
+ end
83
+ end
84
+
85
+ def self.matches(params)
86
+ @team = Team.find_by number: params[:number]
87
+
88
+ if @team
89
+ # Ensure that the competition ID is valid.
90
+ if params[:competition_id]
91
+ @competition = Competition.find params[:competition_id]
92
+
93
+ return [404, ErrorFormatter.format(could_not_find(params[:competition_id], "id", "competition"))] if @competition.nil?
94
+ end
95
+
96
+ @team.matches(params[:competition_id]).to_json
97
+ else
98
+ [404, ErrorFormatter.format(could_not_find(params[:number], "number"))]
99
+ end
100
+ end
101
+
102
+ def self.competitions(params)
103
+ @team = Team.find_by number: params[:number]
104
+
105
+ if @team
106
+ @team.competitions.to_json
107
+ else
108
+ [404, ErrorFormatter.format(could_not_find(params[:number], "number"))]
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,13 @@
1
+ # Libraries that all controllers require.
2
+ require "json"
3
+ require "frecon/error_formatter"
4
+
5
+ require "frecon/controller"
6
+
7
+ require "frecon/controllers/competitions_controller"
8
+ require "frecon/controllers/dump_controller"
9
+ require "frecon/controllers/matches_controller"
10
+ require "frecon/controllers/participations_controller"
11
+ require "frecon/controllers/records_controller"
12
+ require "frecon/controllers/robots_controller"
13
+ require "frecon/controllers/teams_controller"
@@ -0,0 +1,18 @@
1
+ require "logger"
2
+ require "mongoid"
3
+
4
+ require "frecon/models"
5
+
6
+ module FReCon
7
+ class Database
8
+ def self.setup()
9
+ Mongoid.load!(File.join(File.dirname(__FILE__), "mongoid.yml"), ENVIRONMENT)
10
+
11
+ Mongoid.logger.level = Logger::DEBUG
12
+ Mongoid.logger = Logger.new($stdout)
13
+
14
+ Moped.logger.level = Logger::DEBUG
15
+ Moped.logger = Logger.new($stdout)
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ require "json"
2
+
3
+ module FReCon
4
+ class ErrorFormatter
5
+ def self.format(message)
6
+ case message
7
+ when String
8
+ JSON.generate({ errors: [ message ] })
9
+ when Array
10
+ JSON.generate({ errors: message })
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,194 @@
1
+ require "frecon/base"
2
+
3
+ module FReCon
4
+ class MatchNumber
5
+ POSSIBLE_TYPES = [:practice, :qualification, :quarterfinal, :semifinal, :final]
6
+ ELIMINATION_TYPES = [:quarterfinal, :semifinal, :final]
7
+
8
+ attr_reader :number, :round
9
+
10
+ # MongoDB compatibility methods.
11
+ def self.demongoize(object)
12
+ # `object' should *always* be a string (since MatchNumber#mongoize returns a
13
+ # String which is what is stored in the database)
14
+ raise ArgumentError, "`object' must be a String" unless object.is_a?(String)
15
+
16
+ MatchNumber.new(object)
17
+ end
18
+
19
+ def self.mongoize(object)
20
+ case object
21
+ when MatchNumber
22
+ object.mongoize
23
+ when String, Hash
24
+ MatchNumber.new(object).mongoize
25
+ else
26
+ object
27
+ end
28
+ end
29
+
30
+ def self.evolve(object)
31
+ case object
32
+ when MatchNumber
33
+ object.mongoize
34
+ when String, Hash
35
+ MatchNumber.new(object).mongoize
36
+ else
37
+ object
38
+ end
39
+ end
40
+
41
+ def mongoize
42
+ to_s
43
+ end
44
+
45
+ def initialize(args)
46
+ if args.is_a?(String)
47
+ # Match `args' against the regular expression, described below.
48
+ #
49
+ # This regular expression matches all values where the first group of
50
+ # characters is one of either [ "p", "q", "qf", "sf", "f" ], which is
51
+ # parsed as the "type" of the match. This is followed by an "m" and a
52
+ # group of digits, which is parsed as the "number" of the match.
53
+ #
54
+ # In addition, one can specify a "round number" following the first group
55
+ # of characters such as in eliminations and finals. Often times, there
56
+ # are multiple so-called "rounds" in eliminations, and so the system will
57
+ # optionally capture that round.
58
+ #
59
+ # Also, one can specify a "replay number" following the match number.
60
+ # this is done by appending "r" and a group of digits which is the replay
61
+ # number.
62
+ #
63
+ # Below are listed the match groups and what they are:
64
+ #
65
+ # 1: Match type
66
+ # 2: Round number (optional)
67
+ # 3: Match number
68
+ # 4: Replay string (optional)
69
+ # 5: Replay number (required if #4 is supplied)
70
+ #
71
+ # This behavior may change in the future.
72
+ match_data = args.match(/(p|q|qf|sf|f)([\d]+)?m([\d]+)(r)?([\d]+)?/i)
73
+
74
+ # Whine if we don't have a match (string is incorrectly formatted)
75
+ raise ArgumentError, "string is improperly formatted" unless match_data
76
+
77
+ # Check and set required stuff first, everything else later.
78
+
79
+ # Whine if we don't have a match type
80
+ raise ArgumentError, "match type must be supplied" unless match_data[1]
81
+
82
+ # Parse the match type string
83
+ @type = case match_data[1].downcase
84
+ when "p"
85
+ :practice
86
+ when "q"
87
+ :qualification
88
+ when "qf"
89
+ :quarterfinal
90
+ when "sf"
91
+ :semifinal
92
+ when "f"
93
+ :final
94
+ else
95
+ raise ArgumentError, "match type must be in [\"p\", \"q\", \"qf\", \"sf\", \"f\"]"
96
+ end
97
+
98
+ # Whine if we don't have a match number
99
+ raise ArgumentError, "match number must be supplied" unless match_data[3]
100
+
101
+ # Parse the match number
102
+ @number = match_data[3].to_i
103
+ raise ArgumentError, "match number must be greater than 0" unless @number > 0
104
+
105
+ # Parse the round number, if it is present
106
+ if match_data[2]
107
+ @round = match_data[2].to_i
108
+ raise ArgumentError, "round number must be greater than 0" unless @round > 0
109
+ end
110
+
111
+ # Parse replay match group, store replay number if present.
112
+ @replay_number = match_data[5].to_i if match_data[4] == "r"
113
+ elsif args.is_a?(Hash)
114
+ # type (Symbol or String)
115
+ # number (Integer)
116
+ # round (Integer), optional
117
+ # replay_number (Integer), optional
118
+
119
+ raise TypeError, "type must be a Symbol or String" unless args[:type].is_a?(Symbol) || args[:type].is_a?(String)
120
+ raise ArgumentError, "type must be in #{POSSIBLE_TYPES.inspect}" unless POSSIBLE_TYPES.include?(args[:type].to_sym)
121
+
122
+ @type = args[:type].to_sym
123
+
124
+ raise TypeError, "match number must be an Integer" unless args[:number].is_an?(Integer)
125
+ raise ArgumentError, "match number must be greater than 0" unless args[:number] > 0
126
+
127
+ @number = args[:number]
128
+
129
+ if args[:round]
130
+ raise TypeError, "round number must be an Integer" unless args[:round].is_an?(Integer)
131
+ raise ArgumentError, "round number must be greater than 0" unless args[:round] > 0
132
+
133
+ @round = args[:round]
134
+ end
135
+
136
+ if args[:replay_number]
137
+ raise TypeError, "replay number must be an Integer" unless args[:replay_number].is_an?(Integer)
138
+ raise ArgumentError, "replay number must be greater than 0" unless args[:replay_number] > 0
139
+
140
+ @replay_number = args[:replay_number]
141
+ end
142
+ else
143
+ raise TypeError, "argument must be a String or Hash"
144
+ end
145
+ end
146
+
147
+ def to_s
148
+ type_string = case @type
149
+ when :practice
150
+ "p"
151
+ when :qualification
152
+ "q"
153
+ when :quarterfinal
154
+ "qf"
155
+ when :semifinal
156
+ "sf"
157
+ when :final
158
+ "f"
159
+ end
160
+ match_string = "m#{@number}"
161
+ replay_string = "r#{@replay_number}" if replay?
162
+
163
+ "#{type_string}#{@round}#{match_string}#{replay_string}"
164
+ end
165
+
166
+ def replay?
167
+ !@replay_number.nil? && @replay_number > 0
168
+ end
169
+
170
+ def practice?
171
+ @type == :practice
172
+ end
173
+
174
+ def qualification?
175
+ @type == :qualification
176
+ end
177
+
178
+ def quarterfinal?
179
+ @type == :quarterfinal
180
+ end
181
+
182
+ def semifinal?
183
+ @type == :semifinal
184
+ end
185
+
186
+ def final?
187
+ @type == :final
188
+ end
189
+
190
+ def elimination?
191
+ ELIMINATION_TYPES.include?(@type)
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,13 @@
1
+ require "mongoid"
2
+
3
+ module FReCon
4
+ class Model
5
+ def self.inherited(child)
6
+ child.class_eval do
7
+ include Mongoid::Document
8
+ include Mongoid::Timestamps
9
+ include Mongoid::Attributes::Dynamic
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ require "frecon/model"
2
+
3
+ module FReCon
4
+ class Competition < Model
5
+ field :location, type: String
6
+ field :name, type: String
7
+
8
+ has_many :matches, dependent: :destroy
9
+ has_many :participations, dependent: :destroy
10
+
11
+ validates :location, :name, presence: true
12
+ validates :name, uniqueness: true
13
+
14
+ def records
15
+ matches = self.matches
16
+
17
+ Record.in match_id: matches.map(&:id)
18
+ end
19
+
20
+ def teams
21
+ participations = self.participations
22
+
23
+ Team.in id: participations.map(&:team_id)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ require "frecon/model"
2
+ require "frecon/match_number"
3
+
4
+ module FReCon
5
+ class Match < Model
6
+ field :number, type: MatchNumber
7
+
8
+ field :blue_score, type: Integer, default: 0
9
+ field :red_score, type: Integer, default: 0
10
+
11
+ belongs_to :competition
12
+ has_many :records, dependent: :destroy
13
+
14
+ validates :number, :competition_id, presence: true
15
+ end
16
+ end
@@ -0,0 +1,11 @@
1
+ require "frecon/model"
2
+
3
+ module FReCon
4
+ class Participation < Model
5
+ belongs_to :competition
6
+ belongs_to :team
7
+
8
+ validates :competition_id, :team_id, presence: true
9
+ validates :team_id, uniqueness: { scope: :competition_id }
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ require "frecon/model"
2
+ require "frecon/position"
3
+
4
+ module FReCon
5
+ class Record < Model
6
+ field :notes, type: String
7
+ field :position, type: Position
8
+
9
+ belongs_to :match
10
+ belongs_to :team
11
+
12
+ validates :position, :match_id, :team_id, presence: true
13
+
14
+ def competition
15
+ self.match.competition
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,10 @@
1
+ require "frecon/model"
2
+
3
+ module FReCon
4
+ class Robot < Model
5
+ field :name, type: String
6
+
7
+ belongs_to :competition
8
+ belongs_to :team
9
+ end
10
+ end
@@ -0,0 +1,44 @@
1
+ require "frecon/model"
2
+
3
+ module FReCon
4
+ class Team < Model
5
+ field :number, type: Integer
6
+
7
+ field :location, type: String
8
+ field :logo_path, type: String
9
+ field :name, type: String
10
+
11
+ has_many :participations, dependent: :destroy
12
+ has_many :records, dependent: :destroy
13
+
14
+ validates :number, presence: true, uniqueness: true, numericality: { greater_than: 0 }
15
+
16
+ def self.number(team_number)
17
+ # Team.find_by number: team_number
18
+ find_by number: team_number
19
+ end
20
+
21
+ def competitions
22
+ Competition.in id: participations.map(&:competition_id)
23
+ end
24
+
25
+ # Returns all of the matches that this team has been in.
26
+ # Optionally, returns the matches that this team has played
27
+ # in a certain competition.
28
+ def matches(competition_id = nil)
29
+ matches = Match.in record_id: self.records.map(&:id)
30
+ matches = matches.where competition_id: competition_id unless competition_id.nil?
31
+
32
+ matches
33
+ end
34
+
35
+ # alias_method works by default solely on instance
36
+ # methods, so change context to the metaclass of
37
+ # Team and do aliasing there.
38
+ class << self
39
+ alias_method :with_number, :number
40
+ alias_method :that_has_number, :number
41
+ end
42
+
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ require "frecon/models/competition"
2
+ require "frecon/models/match"
3
+ require "frecon/models/participation"
4
+ require "frecon/models/record"
5
+ require "frecon/models/robot"
6
+ require "frecon/models/team"
@@ -0,0 +1,19 @@
1
+ development:
2
+ sessions:
3
+ default:
4
+ database: frecon
5
+ hosts:
6
+ - localhost:27017
7
+ options:
8
+ use_utc: true
9
+ raise_not_found_error: false
10
+
11
+ production:
12
+ sessions:
13
+ default:
14
+ database: frecon
15
+ hosts:
16
+ - localhost:27017
17
+ options:
18
+ use_utc: true
19
+ raise_not_found_error: false
@@ -0,0 +1,117 @@
1
+ require "frecon/base"
2
+
3
+ module FReCon
4
+ class Position
5
+ attr_reader :alliance, :number
6
+
7
+ # MongoDB compatibility methods.
8
+ def self.demongoize(object)
9
+ # `object' should *always* be a string (since MatchNumber#mongoize returns a
10
+ # String which is what is stored in the database)
11
+ raise ArgumentError, "`object' must be a String" unless object.is_a?(String)
12
+
13
+ Position.new(object)
14
+ end
15
+
16
+ # Allows passing a String or Hash instead of a Position.
17
+ # i.e. record.position = "r3"
18
+ def self.mongoize(object)
19
+ case object
20
+ when Position
21
+ object.mongoize
22
+ when String
23
+ Position.new(object).mongoize
24
+ when Hash
25
+ Position.new(object[:alliance], object[:number]).mongoize
26
+ else
27
+ object
28
+ end
29
+ end
30
+
31
+ # Used for queries.
32
+ def self.evolve(object)
33
+ case object
34
+ when Position
35
+ object.mongoize
36
+ when String
37
+ Position.new(object).mongoize
38
+ when Hash
39
+ Position.new(object[:alliance], object[:number]).mongoize
40
+ else
41
+ object
42
+ end
43
+ end
44
+
45
+ def mongoize
46
+ to_s
47
+ end
48
+
49
+ def initialize(*args)
50
+ if args.length == 1
51
+ # Match `string' against the regular expression, described below.
52
+ #
53
+ # This regular expression matches all values for `string' where
54
+ # the first letter is either "r" or "b" (case-insensitive due to /i
55
+ # at the end of the regular expression) and the last one-or-more
56
+ # characters in the string are digits 0-9. Anything between those two
57
+ # that is either a letter or an underscore is not retained, but
58
+ # if other characters exist (e.g. spaces as of right now) `string'
59
+ # will not match.
60
+ #
61
+ # You can use any words you like if you have more than just
62
+ # "r<n>" or "b<n>", for example "red_2" matches just the same
63
+ # as "r2", or, just for fun, just the same as "royal______2".
64
+ #
65
+ # This behavior may change in the future.
66
+ match_data = args[0].match(/^([rb])[a-z\_]*([0-9]+)/i)
67
+
68
+ # Note: if matched at all, match_data[0] is the entire
69
+ # string that was matched, hence the indices that start
70
+ # at one.
71
+
72
+ raise ArgumentError, "string is improperly formatted" unless match_data
73
+
74
+ @alliance = case match_data[1].downcase
75
+ when "b"
76
+ :blue
77
+ when "r"
78
+ :red
79
+ else
80
+ raise ArgumentError, "alliance character must be in [\"b\", \"r\"]"
81
+ end
82
+
83
+ position_number = match_data[2].to_i
84
+ raise ArgumentError, "position number must be in [1, 2, 3]" unless [1, 2, 3].include?(position_number)
85
+
86
+ @number = position_number
87
+ elsif args.length == 2
88
+ raise TypeError, "alliance must be a Symbol or String" unless args[0].is_a?(Symbol) || args[0].is_a?(String)
89
+ raise ArgumentError, "alliance must be in [:blue, :red]" unless [:blue, :red].include?(args[0].to_sym)
90
+
91
+ @alliance = args[0].to_sym
92
+
93
+ raise TypeError, "second argument must be an Integer" unless args[1].is_an?(Integer)
94
+ raise ArgumentError, "second argument must be in [1, 2, 3]" unless [1, 2, 3].include?(args[1])
95
+
96
+ @number = args[1]
97
+ else
98
+ raise ArgumentError, "wrong number of arguments (#{args.length} for [1, 2])"
99
+ end
100
+ end
101
+
102
+ def to_s
103
+ "#{@alliance[0]}#{@number}"
104
+ end
105
+
106
+ def is_blue?
107
+ @alliance == :blue
108
+ end
109
+
110
+ def is_red?
111
+ @alliance == :red
112
+ end
113
+
114
+ alias_method :was_blue?, :is_blue?
115
+ alias_method :was_red?, :is_red?
116
+ end
117
+ end
@@ -0,0 +1,119 @@
1
+ require "frecon/controllers"
2
+
3
+ module FReCon
4
+ module Routes
5
+ def self.resource_routes(base, name, controller, methods = [:create, :update, :delete, :show, :index])
6
+ if methods.include?(:create)
7
+ base.post "/#{name}" do
8
+ controller.create request, params
9
+ end
10
+ end
11
+
12
+ if methods.include?(:update)
13
+ base.put "/#{name}/:id" do
14
+ controller.update request, params
15
+ end
16
+ end
17
+
18
+ if methods.include?(:delete)
19
+ base.delete "/#{name}/:id" do
20
+ controller.delete params
21
+ end
22
+ end
23
+
24
+ if methods.include?(:show)
25
+ base.get "/#{name}/:id" do
26
+ controller.show params
27
+ end
28
+ end
29
+
30
+ if methods.include?(:index)
31
+ base.get "/#{name}" do
32
+ controller.index params
33
+ end
34
+ end
35
+ end
36
+
37
+ def self.included(base)
38
+ resource_routes base, "teams", TeamsController, [:create, :index]
39
+
40
+ base.put "/teams/:number" do
41
+ TeamsController.update request, params
42
+ end
43
+
44
+ base.delete "/teams/:number" do
45
+ TeamsController.delete params
46
+ end
47
+
48
+ base.get "/teams/:number" do
49
+ TeamsController.show params
50
+ end
51
+
52
+ base.get "/teams/:number/records/?:competition_id?" do
53
+ TeamsController.records params
54
+ end
55
+
56
+ base.get "/teams/:number/matches/?:competition_id?" do
57
+ TeamsController.matches params
58
+ end
59
+
60
+ base.get "/teams/:number/competitions" do
61
+ TeamsController.competitions params
62
+ end
63
+
64
+ resource_routes base, "competitions", CompetitionsController
65
+
66
+ base.get "/competitions/:id/teams" do
67
+ CompetitionsController.teams params
68
+ end
69
+
70
+ base.get "/competitions/:id/matches" do
71
+ CompetitionsController.matches params
72
+ end
73
+
74
+ base.get "/competitions/:id/records" do
75
+ CompetitionsController.records params
76
+ end
77
+
78
+ resource_routes base, "matches", MatchesController
79
+
80
+ base.get "/matches/:id/records" do
81
+ MatchesController.records params
82
+ end
83
+
84
+ base.get "/matches/:id/competition" do
85
+ MatchesController.competition params
86
+ end
87
+
88
+ resource_routes base, "records", RecordsController
89
+
90
+ base.get "/records/:id/competition" do
91
+ RecordsController.competition params
92
+ end
93
+
94
+ resource_routes base, "robots", RobotsController
95
+
96
+ base.get "/robots/:id/competition" do
97
+ RobotsController.competition params
98
+ end
99
+
100
+ base.get "/robots/:id/team" do
101
+ RobotsController.team params
102
+ end
103
+
104
+ resource_routes base, "participations", ParticipationsController
105
+
106
+ base.get "/participations/:id/competition" do
107
+ ParticipationsController.competition params
108
+ end
109
+
110
+ base.get "/participations/:id/team" do
111
+ ParticipationsController.team params
112
+ end
113
+
114
+ base.get "/dump" do
115
+ DumpController.full params
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,23 @@
1
+ require "sinatra/base"
2
+
3
+ require "frecon/database"
4
+ require "frecon/routes"
5
+ require "frecon/controllers"
6
+
7
+ module FReCon
8
+ class Server < Sinatra::Base
9
+ include Routes
10
+
11
+ configure do
12
+ Database.setup
13
+ end
14
+
15
+ before do
16
+ content_type "application/json"
17
+ end
18
+
19
+ def self.start
20
+ run!
21
+ end
22
+ end
23
+ end
data/lib/frecon.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "frecon/base"
2
+
3
+ require "frecon/database"
4
+ require "frecon/server"
5
+ require "frecon/console"
metadata ADDED
@@ -0,0 +1,122 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: frecon
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Sam Craig
8
+ - Kristofer Rye
9
+ - Christopher Cooper
10
+ - Sam Mercier
11
+ - Tiger Huang
12
+ - Vincent Mai
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+ date: 2015-09-01 00:00:00.000000000 Z
17
+ dependencies:
18
+ - !ruby/object:Gem::Dependency
19
+ name: sinatra
20
+ requirement: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - "~>"
23
+ - !ruby/object:Gem::Version
24
+ version: '1.4'
25
+ type: :runtime
26
+ prerelease: false
27
+ version_requirements: !ruby/object:Gem::Requirement
28
+ requirements:
29
+ - - "~>"
30
+ - !ruby/object:Gem::Version
31
+ version: '1.4'
32
+ - !ruby/object:Gem::Dependency
33
+ name: thin
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '1.6'
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - "~>"
44
+ - !ruby/object:Gem::Version
45
+ version: '1.6'
46
+ - !ruby/object:Gem::Dependency
47
+ name: mongoid
48
+ requirement: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '4.0'
53
+ type: :runtime
54
+ prerelease: false
55
+ version_requirements: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - "~>"
58
+ - !ruby/object:Gem::Version
59
+ version: '4.0'
60
+ description: A JSON API for scouting FRC competitions that manages the database for
61
+ the user.
62
+ email: sammidysam@gmail.com
63
+ executables:
64
+ - frecon
65
+ extensions: []
66
+ extra_rdoc_files: []
67
+ files:
68
+ - bin/frecon
69
+ - lib/frecon.rb
70
+ - lib/frecon/base.rb
71
+ - lib/frecon/base/object.rb
72
+ - lib/frecon/base/variables.rb
73
+ - lib/frecon/console.rb
74
+ - lib/frecon/controller.rb
75
+ - lib/frecon/controllers.rb
76
+ - lib/frecon/controllers/competitions_controller.rb
77
+ - lib/frecon/controllers/dump_controller.rb
78
+ - lib/frecon/controllers/matches_controller.rb
79
+ - lib/frecon/controllers/participations_controller.rb
80
+ - lib/frecon/controllers/records_controller.rb
81
+ - lib/frecon/controllers/robots_controller.rb
82
+ - lib/frecon/controllers/teams_controller.rb
83
+ - lib/frecon/database.rb
84
+ - lib/frecon/error_formatter.rb
85
+ - lib/frecon/match_number.rb
86
+ - lib/frecon/model.rb
87
+ - lib/frecon/models.rb
88
+ - lib/frecon/models/competition.rb
89
+ - lib/frecon/models/match.rb
90
+ - lib/frecon/models/participation.rb
91
+ - lib/frecon/models/record.rb
92
+ - lib/frecon/models/robot.rb
93
+ - lib/frecon/models/team.rb
94
+ - lib/frecon/mongoid.yml
95
+ - lib/frecon/position.rb
96
+ - lib/frecon/routes.rb
97
+ - lib/frecon/server.rb
98
+ homepage: https://github.com/scouting-project/scouting-project
99
+ licenses: []
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.4.5.1
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: A JSON API for scouting FRC competitions.
121
+ test_files: []
122
+ has_rdoc: